@minkinad/api-sdk-generator-core 0.2.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/LICENSE +21 -0
- package/README.md +32 -0
- package/dist/index.cjs +1093 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +143 -0
- package/dist/index.d.ts +143 -0
- package/dist/index.js +1032 -0
- package/dist/index.js.map +1 -0
- package/package.json +65 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1093 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
ApiSdkGeneratorError: () => ApiSdkGeneratorError,
|
|
34
|
+
OutputWriteError: () => OutputWriteError,
|
|
35
|
+
SchemaLoadError: () => SchemaLoadError,
|
|
36
|
+
SchemaValidationError: () => SchemaValidationError,
|
|
37
|
+
UnsupportedSchemaError: () => UnsupportedSchemaError,
|
|
38
|
+
createFunctionName: () => createFunctionName,
|
|
39
|
+
createLogger: () => createLogger,
|
|
40
|
+
deriveSdkName: () => deriveSdkName,
|
|
41
|
+
generateSdk: () => generateSdk,
|
|
42
|
+
getReferenceName: () => getReferenceName,
|
|
43
|
+
isReferenceObject: () => isReferenceObject,
|
|
44
|
+
isSchemaObject: () => isSchemaObject,
|
|
45
|
+
loadOpenApiDocument: () => loadOpenApiDocument,
|
|
46
|
+
loadOpenApiDocumentFromFile: () => loadOpenApiDocumentFromFile,
|
|
47
|
+
loadOpenApiDocumentFromUrl: () => loadOpenApiDocumentFromUrl,
|
|
48
|
+
noopLogger: () => noopLogger,
|
|
49
|
+
parseDocument: () => parseDocument,
|
|
50
|
+
resolveLocalComponent: () => resolveLocalComponent,
|
|
51
|
+
resolveSchema: () => resolveSchema,
|
|
52
|
+
sanitizeIdentifier: () => sanitizeIdentifier,
|
|
53
|
+
toCamelCase: () => toCamelCase,
|
|
54
|
+
toPascalCase: () => toPascalCase,
|
|
55
|
+
toPropertyAccessor: () => toPropertyAccessor,
|
|
56
|
+
toTypeName: () => toTypeName,
|
|
57
|
+
validateOpenApiDocument: () => validateOpenApiDocument
|
|
58
|
+
});
|
|
59
|
+
module.exports = __toCommonJS(index_exports);
|
|
60
|
+
|
|
61
|
+
// src/errors.ts
|
|
62
|
+
var ApiSdkGeneratorError = class extends Error {
|
|
63
|
+
code;
|
|
64
|
+
constructor(message, code = "API_SDK_GENERATOR_ERROR", cause) {
|
|
65
|
+
super(message, cause instanceof Error ? { cause } : void 0);
|
|
66
|
+
this.name = new.target.name;
|
|
67
|
+
this.code = code;
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
var SchemaLoadError = class extends ApiSdkGeneratorError {
|
|
71
|
+
constructor(message, cause) {
|
|
72
|
+
super(message, "SCHEMA_LOAD_ERROR", cause);
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
var SchemaValidationError = class extends ApiSdkGeneratorError {
|
|
76
|
+
constructor(message, cause) {
|
|
77
|
+
super(message, "SCHEMA_VALIDATION_ERROR", cause);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
var UnsupportedSchemaError = class extends ApiSdkGeneratorError {
|
|
81
|
+
constructor(message, cause) {
|
|
82
|
+
super(message, "UNSUPPORTED_SCHEMA_ERROR", cause);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var OutputWriteError = class extends ApiSdkGeneratorError {
|
|
86
|
+
constructor(message, cause) {
|
|
87
|
+
super(message, "OUTPUT_WRITE_ERROR", cause);
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
// src/generate-sdk.ts
|
|
92
|
+
var import_node_path3 = __toESM(require("path"), 1);
|
|
93
|
+
|
|
94
|
+
// src/formatter.ts
|
|
95
|
+
var import_prettier = __toESM(require("prettier"), 1);
|
|
96
|
+
var GENERATED_FORMAT_OPTIONS = {
|
|
97
|
+
printWidth: 100,
|
|
98
|
+
semi: true,
|
|
99
|
+
singleQuote: true,
|
|
100
|
+
trailingComma: "all"
|
|
101
|
+
};
|
|
102
|
+
function getParser(filePath) {
|
|
103
|
+
return filePath.endsWith(".md") ? "markdown" : "typescript";
|
|
104
|
+
}
|
|
105
|
+
async function formatGeneratedFiles(files) {
|
|
106
|
+
return Promise.all(
|
|
107
|
+
files.map(async (file) => ({
|
|
108
|
+
...file,
|
|
109
|
+
content: await import_prettier.default.format(file.content, {
|
|
110
|
+
...GENERATED_FORMAT_OPTIONS,
|
|
111
|
+
parser: getParser(file.path)
|
|
112
|
+
})
|
|
113
|
+
}))
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/naming.ts
|
|
118
|
+
var RESERVED_WORDS = /* @__PURE__ */ new Set([
|
|
119
|
+
"default",
|
|
120
|
+
"function",
|
|
121
|
+
"class",
|
|
122
|
+
"switch",
|
|
123
|
+
"case",
|
|
124
|
+
"var",
|
|
125
|
+
"const",
|
|
126
|
+
"let",
|
|
127
|
+
"new",
|
|
128
|
+
"delete",
|
|
129
|
+
"return"
|
|
130
|
+
]);
|
|
131
|
+
function splitWords(input) {
|
|
132
|
+
return input.replace(/([a-z0-9])([A-Z])/g, "$1 $2").split(/[^A-Za-z0-9]+/).map((part) => part.trim()).filter(Boolean);
|
|
133
|
+
}
|
|
134
|
+
function capitalize(value) {
|
|
135
|
+
return value.charAt(0).toUpperCase() + value.slice(1);
|
|
136
|
+
}
|
|
137
|
+
function toPascalCase(input) {
|
|
138
|
+
const words = splitWords(input);
|
|
139
|
+
const pascal = words.map((word) => capitalize(word.toLowerCase())).join("");
|
|
140
|
+
return pascal || "GeneratedType";
|
|
141
|
+
}
|
|
142
|
+
function toCamelCase(input) {
|
|
143
|
+
const pascal = toPascalCase(input);
|
|
144
|
+
return pascal.charAt(0).toLowerCase() + pascal.slice(1);
|
|
145
|
+
}
|
|
146
|
+
function sanitizeIdentifier(input) {
|
|
147
|
+
const camel = toCamelCase(input).replace(/[^A-Za-z0-9_$]/g, "");
|
|
148
|
+
const normalized = camel.match(/^[A-Za-z_$]/) ? camel : `_${camel}`;
|
|
149
|
+
return RESERVED_WORDS.has(normalized) ? `${normalized}Value` : normalized;
|
|
150
|
+
}
|
|
151
|
+
function toTypeName(input) {
|
|
152
|
+
const pascal = toPascalCase(input).replace(/[^A-Za-z0-9_$]/g, "");
|
|
153
|
+
return pascal.match(/^[A-Za-z_$]/) ? pascal : `T${pascal}`;
|
|
154
|
+
}
|
|
155
|
+
function toPropertyAccessor(name) {
|
|
156
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? name : JSON.stringify(name);
|
|
157
|
+
}
|
|
158
|
+
function createFunctionName(method, path4, operationId) {
|
|
159
|
+
if (operationId) {
|
|
160
|
+
return sanitizeIdentifier(operationId);
|
|
161
|
+
}
|
|
162
|
+
const segments = path4.split("/").filter(Boolean);
|
|
163
|
+
const pieces = [method.toLowerCase()];
|
|
164
|
+
const paramNames = [];
|
|
165
|
+
for (const segment of segments) {
|
|
166
|
+
const match = segment.match(/^\{(.+)\}$/);
|
|
167
|
+
if (match) {
|
|
168
|
+
paramNames.push(toPascalCase(match[1]));
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
pieces.push(toPascalCase(segment));
|
|
172
|
+
}
|
|
173
|
+
if (paramNames.length > 0) {
|
|
174
|
+
pieces.push(`By${paramNames.join("And")}`);
|
|
175
|
+
}
|
|
176
|
+
return sanitizeIdentifier(pieces.join(" "));
|
|
177
|
+
}
|
|
178
|
+
function deriveSdkName(input) {
|
|
179
|
+
return input ? toTypeName(input) : "GeneratedSdk";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// src/generator/client-generator.ts
|
|
183
|
+
function renderTemplatePath(path4, operation) {
|
|
184
|
+
const parts = [];
|
|
185
|
+
let offset = 0;
|
|
186
|
+
for (const match of path4.matchAll(/\{([^}]+)\}/g)) {
|
|
187
|
+
const parameter = operation.pathParameters.find((item) => item.name === match[1]);
|
|
188
|
+
if (!parameter) continue;
|
|
189
|
+
parts.push(JSON.stringify(path4.slice(offset, match.index)));
|
|
190
|
+
parts.push(`encodeURIComponent(String(request[${JSON.stringify(parameter.name)}]))`);
|
|
191
|
+
offset = match.index + match[0].length;
|
|
192
|
+
}
|
|
193
|
+
parts.push(JSON.stringify(path4.slice(offset)));
|
|
194
|
+
return parts.join(" + ");
|
|
195
|
+
}
|
|
196
|
+
function renderQueryLines(operation) {
|
|
197
|
+
return operation.queryParameters.map((parameter) => {
|
|
198
|
+
const style = parameter.style ?? "form";
|
|
199
|
+
const explode = parameter.explode ?? style === "form";
|
|
200
|
+
const separator = style === "spaceDelimited" ? " " : style === "pipeDelimited" ? "|" : ",";
|
|
201
|
+
return ` appendQueryParameter(searchParams, ${JSON.stringify(parameter.name)}, request[${JSON.stringify(parameter.name)}], ${explode}, ${JSON.stringify(separator)});`;
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
function renderMethodSignature(operation) {
|
|
205
|
+
if (!operation.hasRequestShape) {
|
|
206
|
+
return `${operation.functionName}(init?: RequestInit): Promise<${operation.responseTypeName}>;`;
|
|
207
|
+
}
|
|
208
|
+
if (operation.hasRequiredRequestFields) {
|
|
209
|
+
return `${operation.functionName}(request: ${operation.requestTypeName}, init?: RequestInit): Promise<${operation.responseTypeName}>;`;
|
|
210
|
+
}
|
|
211
|
+
return `${operation.functionName}(request?: ${operation.requestTypeName}, init?: RequestInit): Promise<${operation.responseTypeName}>;`;
|
|
212
|
+
}
|
|
213
|
+
function renderMethodImplementation(operation) {
|
|
214
|
+
const requestParameter = operation.hasRequestShape ? operation.hasRequiredRequestFields ? "request" : "request = {}" : "";
|
|
215
|
+
const queryLines = renderQueryLines(operation);
|
|
216
|
+
const bodyContent = operation.requestBody?.contentType === "application/json" || operation.requestBody?.contentType?.endsWith("+json") ? [
|
|
217
|
+
" const body = request.body !== undefined ? JSON.stringify(request.body) : undefined;",
|
|
218
|
+
' if (body !== undefined && !headers.has("content-type")) {',
|
|
219
|
+
` headers.set("content-type", ${JSON.stringify(operation.requestBody.contentType)});`,
|
|
220
|
+
" }"
|
|
221
|
+
].join("\n") : " const body = undefined;";
|
|
222
|
+
return [
|
|
223
|
+
` async ${operation.functionName}(${requestParameter}${requestParameter ? ", " : ""}init?: RequestInit): Promise<${operation.responseTypeName}> {`,
|
|
224
|
+
` const url = resolveRequestUrl(resolveBaseUrl(config.baseUrl), ${renderTemplatePath(operation.path, operation)});`,
|
|
225
|
+
" const searchParams = url.searchParams;",
|
|
226
|
+
...queryLines.length > 0 ? queryLines : [" void searchParams;"],
|
|
227
|
+
" const headers = new Headers(config.headers);",
|
|
228
|
+
bodyContent,
|
|
229
|
+
" const response = await runtimeFetch(url, {",
|
|
230
|
+
" ...init,",
|
|
231
|
+
` method: ${JSON.stringify(operation.method.toUpperCase())},`,
|
|
232
|
+
" body,",
|
|
233
|
+
" headers: mergeHeaders(headers, init?.headers),",
|
|
234
|
+
" });",
|
|
235
|
+
"",
|
|
236
|
+
" if (!response.ok) {",
|
|
237
|
+
" throw await ApiError.fromResponse(response);",
|
|
238
|
+
" }",
|
|
239
|
+
"",
|
|
240
|
+
` return parseResponse<${operation.responseTypeName}>(response);`,
|
|
241
|
+
" },"
|
|
242
|
+
].join("\n");
|
|
243
|
+
}
|
|
244
|
+
function generateClientSource(parsed) {
|
|
245
|
+
const clientName = `${toTypeName(parsed.sdkName)}Client`;
|
|
246
|
+
const interfaceLines = parsed.operations.map(
|
|
247
|
+
(operation) => ` ${renderMethodSignature(operation)}`
|
|
248
|
+
);
|
|
249
|
+
const methodLines = parsed.operations.map((operation) => renderMethodImplementation(operation));
|
|
250
|
+
return [
|
|
251
|
+
"/* eslint-disable */",
|
|
252
|
+
"/**",
|
|
253
|
+
" * Auto-generated by api-sdk-generator.",
|
|
254
|
+
" * Do not edit manually.",
|
|
255
|
+
" */",
|
|
256
|
+
"",
|
|
257
|
+
...parsed.operations.length > 0 ? [
|
|
258
|
+
"import type {",
|
|
259
|
+
...parsed.operations.flatMap((operation) => [
|
|
260
|
+
...operation.hasRequestShape ? [` ${operation.requestTypeName},`] : [],
|
|
261
|
+
` ${operation.responseTypeName},`
|
|
262
|
+
]),
|
|
263
|
+
"} from './types.js';",
|
|
264
|
+
""
|
|
265
|
+
] : [],
|
|
266
|
+
"export interface ClientConfig {",
|
|
267
|
+
" baseUrl?: string;",
|
|
268
|
+
" fetch?: typeof fetch;",
|
|
269
|
+
" headers?: HeadersInit;",
|
|
270
|
+
"}",
|
|
271
|
+
"",
|
|
272
|
+
`export interface ${clientName} {`,
|
|
273
|
+
...interfaceLines,
|
|
274
|
+
"}",
|
|
275
|
+
"",
|
|
276
|
+
"export class ApiError extends Error {",
|
|
277
|
+
" public readonly body: unknown;",
|
|
278
|
+
" public readonly status: number;",
|
|
279
|
+
" public readonly headers: Headers;",
|
|
280
|
+
"",
|
|
281
|
+
" public constructor(status: number, message: string, body: unknown, headers: Headers = new Headers()) {",
|
|
282
|
+
" super(message);",
|
|
283
|
+
' this.name = "ApiError";',
|
|
284
|
+
" this.status = status;",
|
|
285
|
+
" this.body = body;",
|
|
286
|
+
" this.headers = headers;",
|
|
287
|
+
" }",
|
|
288
|
+
"",
|
|
289
|
+
" public static async fromResponse(response: Response): Promise<ApiError> {",
|
|
290
|
+
" const body = await parseResponseBody(response.clone()).catch(() => response.text());",
|
|
291
|
+
" return new ApiError(response.status, `Request failed with status ${response.status}`, body, response.headers);",
|
|
292
|
+
" }",
|
|
293
|
+
"}",
|
|
294
|
+
"",
|
|
295
|
+
"export function createClient(config: ClientConfig = {}): " + clientName + " {",
|
|
296
|
+
" const runtimeFetch = config.fetch ?? globalThis.fetch;",
|
|
297
|
+
"",
|
|
298
|
+
" if (!runtimeFetch) {",
|
|
299
|
+
' throw new Error("Fetch API is not available in the current runtime.");',
|
|
300
|
+
" }",
|
|
301
|
+
"",
|
|
302
|
+
" return {",
|
|
303
|
+
...methodLines,
|
|
304
|
+
" };",
|
|
305
|
+
"}",
|
|
306
|
+
"",
|
|
307
|
+
...parsed.operations.length > 0 ? [
|
|
308
|
+
"function resolveBaseUrl(baseUrl?: string): string {",
|
|
309
|
+
` return baseUrl ?? ${JSON.stringify(parsed.defaultBaseUrl ?? "http://localhost")};`,
|
|
310
|
+
"}",
|
|
311
|
+
"",
|
|
312
|
+
"function resolveRequestUrl(baseUrl: string, requestPath: string): URL {",
|
|
313
|
+
' const normalizedBaseUrl = baseUrl.endsWith("/") ? baseUrl : `${baseUrl}/`;',
|
|
314
|
+
' return new URL(requestPath.replace(/^\\/+/, ""), normalizedBaseUrl);',
|
|
315
|
+
"}",
|
|
316
|
+
"",
|
|
317
|
+
"async function parseResponse<T>(response: Response): Promise<T> {",
|
|
318
|
+
" return (await parseResponseBody(response)) as T;",
|
|
319
|
+
"}",
|
|
320
|
+
""
|
|
321
|
+
] : [],
|
|
322
|
+
"async function parseResponseBody(response: Response): Promise<unknown> {",
|
|
323
|
+
" if (response.status === 204 || response.status === 205) {",
|
|
324
|
+
" return undefined;",
|
|
325
|
+
" }",
|
|
326
|
+
"",
|
|
327
|
+
" const body = await response.text();",
|
|
328
|
+
"",
|
|
329
|
+
" if (!body) {",
|
|
330
|
+
" return undefined;",
|
|
331
|
+
" }",
|
|
332
|
+
"",
|
|
333
|
+
' const contentType = response.headers.get("content-type") ?? "";',
|
|
334
|
+
" return isJsonContentType(contentType) ? (JSON.parse(body) as unknown) : body;",
|
|
335
|
+
"}",
|
|
336
|
+
"",
|
|
337
|
+
"function isJsonContentType(contentType: string): boolean {",
|
|
338
|
+
' const mediaType = contentType.split(";", 1)[0].trim().toLowerCase();',
|
|
339
|
+
' return mediaType === "application/json" || mediaType.endsWith("+json");',
|
|
340
|
+
"}",
|
|
341
|
+
"",
|
|
342
|
+
...parsed.operations.some((operation) => operation.queryParameters.length > 0) ? [
|
|
343
|
+
"function appendQueryParameter(params: URLSearchParams, name: string, value: unknown, explode: boolean, separator: string): void {",
|
|
344
|
+
" if (value === undefined) return;",
|
|
345
|
+
" if (Array.isArray(value)) {",
|
|
346
|
+
" if (explode) {",
|
|
347
|
+
" for (const item of value) params.append(name, String(item));",
|
|
348
|
+
" } else {",
|
|
349
|
+
" params.set(name, value.map(String).join(separator));",
|
|
350
|
+
" }",
|
|
351
|
+
" } else {",
|
|
352
|
+
" params.set(name, String(value));",
|
|
353
|
+
" }",
|
|
354
|
+
"}",
|
|
355
|
+
""
|
|
356
|
+
] : [],
|
|
357
|
+
...parsed.operations.length > 0 ? [
|
|
358
|
+
"function mergeHeaders(baseHeaders: Headers, initHeaders?: HeadersInit): Headers {",
|
|
359
|
+
" const merged = new Headers(baseHeaders);",
|
|
360
|
+
"",
|
|
361
|
+
" if (initHeaders) {",
|
|
362
|
+
" const overlay = new Headers(initHeaders);",
|
|
363
|
+
" overlay.forEach((value, key) => merged.set(key, value));",
|
|
364
|
+
" }",
|
|
365
|
+
"",
|
|
366
|
+
" return merged;",
|
|
367
|
+
"}",
|
|
368
|
+
""
|
|
369
|
+
] : []
|
|
370
|
+
].join("\n");
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// src/generator/index-generator.ts
|
|
374
|
+
function generateIndexSource() {
|
|
375
|
+
return ["export * from './types.js';", "export * from './client.js';", ""].join("\n");
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// src/generator/readme-generator.ts
|
|
379
|
+
function generateGeneratedReadme(parsed) {
|
|
380
|
+
const exampleOperation = parsed.operations[0];
|
|
381
|
+
const exampleCall = exampleOperation?.hasRequestShape ? `${exampleOperation.functionName}({ /* request */ })` : `${exampleOperation?.functionName ?? "listResources"}()`;
|
|
382
|
+
return [
|
|
383
|
+
`# ${parsed.sdkName}`,
|
|
384
|
+
"",
|
|
385
|
+
"This SDK was generated by `api-sdk-generator`.",
|
|
386
|
+
"",
|
|
387
|
+
"## Usage",
|
|
388
|
+
"",
|
|
389
|
+
"```ts",
|
|
390
|
+
"import { createClient } from './index';",
|
|
391
|
+
"",
|
|
392
|
+
"const client = createClient({",
|
|
393
|
+
` baseUrl: ${JSON.stringify(parsed.defaultBaseUrl ?? "https://api.example.com")},`,
|
|
394
|
+
"});",
|
|
395
|
+
"",
|
|
396
|
+
`await client.${exampleCall};`,
|
|
397
|
+
"```",
|
|
398
|
+
""
|
|
399
|
+
].join("\n");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// src/resolver.ts
|
|
403
|
+
function isReferenceObject(schema) {
|
|
404
|
+
return "$ref" in schema;
|
|
405
|
+
}
|
|
406
|
+
function getReferenceName(ref) {
|
|
407
|
+
const match = ref.match(/^#\/components\/schemas\/(.+)$/);
|
|
408
|
+
if (!match) {
|
|
409
|
+
throw new SchemaValidationError(
|
|
410
|
+
`Only local component schema refs are supported. Received "${ref}".`
|
|
411
|
+
);
|
|
412
|
+
}
|
|
413
|
+
return decodePointerToken(match[1]);
|
|
414
|
+
}
|
|
415
|
+
function resolveSchema(document, schema) {
|
|
416
|
+
if (!isReferenceObject(schema)) {
|
|
417
|
+
return schema;
|
|
418
|
+
}
|
|
419
|
+
return resolveLocalComponent(
|
|
420
|
+
schema,
|
|
421
|
+
document.components?.schemas,
|
|
422
|
+
"schemas"
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
function decodePointerToken(token) {
|
|
426
|
+
return token.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
427
|
+
}
|
|
428
|
+
function resolveLocalComponent(reference, collection, section) {
|
|
429
|
+
const seen = /* @__PURE__ */ new Set();
|
|
430
|
+
let current = reference;
|
|
431
|
+
const prefix = `#/components/${section}/`;
|
|
432
|
+
for (; ; ) {
|
|
433
|
+
if (!current.$ref.startsWith(prefix)) {
|
|
434
|
+
throw new SchemaValidationError(
|
|
435
|
+
`Only local ${section} refs are supported. Received "${current.$ref}".`
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
if (seen.has(current.$ref)) {
|
|
439
|
+
throw new SchemaValidationError(`Circular component reference "${current.$ref}".`);
|
|
440
|
+
}
|
|
441
|
+
seen.add(current.$ref);
|
|
442
|
+
const name = decodePointerToken(current.$ref.slice(prefix.length));
|
|
443
|
+
const resolved = collection && Object.hasOwn(collection, name) ? collection[name] : void 0;
|
|
444
|
+
if (!resolved) {
|
|
445
|
+
throw new SchemaValidationError(`Unable to resolve ${section} reference "${current.$ref}".`);
|
|
446
|
+
}
|
|
447
|
+
if ("$ref" in resolved) {
|
|
448
|
+
current = resolved;
|
|
449
|
+
} else {
|
|
450
|
+
return resolved;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// src/validator.ts
|
|
456
|
+
function isObject(value) {
|
|
457
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
458
|
+
}
|
|
459
|
+
function validateOpenApiDocument(value) {
|
|
460
|
+
if (!isObject(value)) {
|
|
461
|
+
throw new SchemaValidationError("OpenAPI document must be a JSON object.");
|
|
462
|
+
}
|
|
463
|
+
if (typeof value.openapi !== "string" || !value.openapi.startsWith("3.")) {
|
|
464
|
+
throw new SchemaValidationError("Only OpenAPI 3.x documents are supported.");
|
|
465
|
+
}
|
|
466
|
+
if (!isObject(value.info) || typeof value.info.title !== "string") {
|
|
467
|
+
throw new SchemaValidationError("OpenAPI document must contain info.title.");
|
|
468
|
+
}
|
|
469
|
+
if (!isObject(value.paths)) {
|
|
470
|
+
throw new SchemaValidationError("OpenAPI document must contain a paths object.");
|
|
471
|
+
}
|
|
472
|
+
return value;
|
|
473
|
+
}
|
|
474
|
+
function isSchemaObject(value) {
|
|
475
|
+
return isObject(value) && !("$ref" in value);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/generator/type-generator.ts
|
|
479
|
+
function quoteEnumValue(value) {
|
|
480
|
+
return typeof value === "string" ? JSON.stringify(value) : String(value);
|
|
481
|
+
}
|
|
482
|
+
function normalizeNullable(rendered, schema) {
|
|
483
|
+
return schema.nullable ? `${rendered} | null` : rendered;
|
|
484
|
+
}
|
|
485
|
+
function renderObjectType(schema, context) {
|
|
486
|
+
const required = new Set(schema.required ?? []);
|
|
487
|
+
const properties = schema.properties ?? {};
|
|
488
|
+
const propertyEntries = Object.entries(properties);
|
|
489
|
+
if (propertyEntries.length === 0) {
|
|
490
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") {
|
|
491
|
+
return `Record<string, ${renderSchema(schema.additionalProperties, context)}>`;
|
|
492
|
+
}
|
|
493
|
+
return schema.additionalProperties === false ? "Record<string, never>" : "Record<string, unknown>";
|
|
494
|
+
}
|
|
495
|
+
const lines = propertyEntries.map(([name, propertySchema]) => {
|
|
496
|
+
const propertyType = renderSchema(propertySchema, context);
|
|
497
|
+
const optionalToken = required.has(name) ? "" : "?";
|
|
498
|
+
return ` ${toPropertyAccessor(name)}${optionalToken}: ${propertyType};`;
|
|
499
|
+
});
|
|
500
|
+
const objectType = ["{", ...lines, "}"].join("\n");
|
|
501
|
+
if (schema.additionalProperties) {
|
|
502
|
+
const valueType = schema.additionalProperties === true ? "unknown" : renderSchema(schema.additionalProperties, context);
|
|
503
|
+
return `${objectType} & Record<string, ${valueType}>`;
|
|
504
|
+
}
|
|
505
|
+
return objectType;
|
|
506
|
+
}
|
|
507
|
+
function renderSchema(schema, context) {
|
|
508
|
+
if (isReferenceObject(schema)) {
|
|
509
|
+
resolveSchema(context.document, schema);
|
|
510
|
+
return toTypeName(getReferenceName(schema.$ref));
|
|
511
|
+
}
|
|
512
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
513
|
+
return normalizeNullable(schema.enum.map(quoteEnumValue).join(" | "), schema);
|
|
514
|
+
}
|
|
515
|
+
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
516
|
+
return normalizeNullable(
|
|
517
|
+
schema.oneOf.map((item) => renderSchema(item, context)).join(" | "),
|
|
518
|
+
schema
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
522
|
+
return normalizeNullable(
|
|
523
|
+
schema.anyOf.map((item) => `(${renderSchema(item, context)})`).join(" | "),
|
|
524
|
+
schema
|
|
525
|
+
);
|
|
526
|
+
}
|
|
527
|
+
if (schema.allOf && schema.allOf.length > 0) {
|
|
528
|
+
return normalizeNullable(
|
|
529
|
+
schema.allOf.map((item) => `(${renderSchema(item, context)})`).join(" & "),
|
|
530
|
+
schema
|
|
531
|
+
);
|
|
532
|
+
}
|
|
533
|
+
if (schema.type === "array") {
|
|
534
|
+
if (!schema.items) {
|
|
535
|
+
throw new UnsupportedSchemaError("Array schema must define items.");
|
|
536
|
+
}
|
|
537
|
+
return normalizeNullable(`Array<${renderSchema(schema.items, context)}>`, schema);
|
|
538
|
+
}
|
|
539
|
+
if (schema.type === "object" || schema.properties || schema.additionalProperties) {
|
|
540
|
+
return normalizeNullable(renderObjectType(schema, context), schema);
|
|
541
|
+
}
|
|
542
|
+
if (schema.type === "string") {
|
|
543
|
+
return normalizeNullable("string", schema);
|
|
544
|
+
}
|
|
545
|
+
if (schema.type === "number" || schema.type === "integer") {
|
|
546
|
+
return normalizeNullable("number", schema);
|
|
547
|
+
}
|
|
548
|
+
if (schema.type === "boolean") {
|
|
549
|
+
return normalizeNullable("boolean", schema);
|
|
550
|
+
}
|
|
551
|
+
return "unknown";
|
|
552
|
+
}
|
|
553
|
+
function renderNamedSchemaExport(name, schema, context) {
|
|
554
|
+
if (isReferenceObject(schema)) {
|
|
555
|
+
return `export type ${toTypeName(name)} = ${renderSchema(schema, context)};`;
|
|
556
|
+
}
|
|
557
|
+
const resolved = resolveSchema(context.document, schema);
|
|
558
|
+
const rendered = renderSchema(resolved, context);
|
|
559
|
+
if (rendered.startsWith("{") && rendered.endsWith("}") && !resolved.nullable && !resolved.allOf && !resolved.oneOf && !resolved.anyOf && !resolved.additionalProperties) {
|
|
560
|
+
return `export interface ${toTypeName(name)} ${rendered}`;
|
|
561
|
+
}
|
|
562
|
+
return `export type ${toTypeName(name)} = ${rendered};`;
|
|
563
|
+
}
|
|
564
|
+
function createRequestSchema(operation) {
|
|
565
|
+
const properties = /* @__PURE__ */ Object.create(null);
|
|
566
|
+
const required = [];
|
|
567
|
+
for (const parameter of [...operation.pathParameters, ...operation.queryParameters]) {
|
|
568
|
+
properties[parameter.name] = parameter.schema;
|
|
569
|
+
if (parameter.required) {
|
|
570
|
+
required.push(parameter.name);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
if (operation.requestBody) {
|
|
574
|
+
properties.body = operation.requestBody.schema;
|
|
575
|
+
if (operation.requestBody.required) {
|
|
576
|
+
required.push("body");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
if (Object.keys(properties).length === 0) {
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
582
|
+
return {
|
|
583
|
+
properties,
|
|
584
|
+
required,
|
|
585
|
+
type: "object"
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function createResponseSchema(operation) {
|
|
589
|
+
return operation.response.schema;
|
|
590
|
+
}
|
|
591
|
+
function renderOperationTypes(parsed, context) {
|
|
592
|
+
const blocks = [];
|
|
593
|
+
for (const operation of parsed.operations) {
|
|
594
|
+
const requestSchema = createRequestSchema(operation);
|
|
595
|
+
if (requestSchema) {
|
|
596
|
+
blocks.push(renderNamedSchemaExport(operation.requestTypeName, requestSchema, context));
|
|
597
|
+
} else {
|
|
598
|
+
blocks.push(`export type ${operation.requestTypeName} = void;`);
|
|
599
|
+
}
|
|
600
|
+
const responseSchema = createResponseSchema(operation);
|
|
601
|
+
if (responseSchema) {
|
|
602
|
+
blocks.push(renderNamedSchemaExport(operation.responseTypeName, responseSchema, context));
|
|
603
|
+
} else {
|
|
604
|
+
blocks.push(`export type ${operation.responseTypeName} = void;`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
return blocks;
|
|
608
|
+
}
|
|
609
|
+
function generateTypesSource(parsed) {
|
|
610
|
+
const context = {
|
|
611
|
+
document: parsed.document
|
|
612
|
+
};
|
|
613
|
+
const componentBlocks = Object.entries(parsed.componentSchemas).sort(([left], [right]) => left.localeCompare(right)).filter(([, schema]) => isReferenceObject(schema) || isSchemaObject(schema)).map(([name, schema]) => renderNamedSchemaExport(name, schema, context));
|
|
614
|
+
const operationBlocks = renderOperationTypes(parsed, context);
|
|
615
|
+
return [
|
|
616
|
+
"/* eslint-disable */",
|
|
617
|
+
"/**",
|
|
618
|
+
" * Auto-generated by api-sdk-generator.",
|
|
619
|
+
" * Do not edit manually.",
|
|
620
|
+
" */",
|
|
621
|
+
"",
|
|
622
|
+
"export {};",
|
|
623
|
+
"",
|
|
624
|
+
...componentBlocks,
|
|
625
|
+
"",
|
|
626
|
+
...operationBlocks,
|
|
627
|
+
""
|
|
628
|
+
].join("\n");
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// src/loader.ts
|
|
632
|
+
var import_promises = require("fs/promises");
|
|
633
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
634
|
+
var import_yaml = require("yaml");
|
|
635
|
+
function parseSchema(content) {
|
|
636
|
+
const parsed = (0, import_yaml.parseDocument)(content, { uniqueKeys: true });
|
|
637
|
+
if (parsed.errors.length > 0) {
|
|
638
|
+
throw new SchemaLoadError(`Invalid JSON or YAML: ${parsed.errors[0].message}`);
|
|
639
|
+
}
|
|
640
|
+
return validateOpenApiDocument(parsed.toJS({ maxAliasCount: 100 }));
|
|
641
|
+
}
|
|
642
|
+
async function loadOpenApiDocument(input, options = {}) {
|
|
643
|
+
const sources = [input.file, input.url].filter(Boolean);
|
|
644
|
+
if (sources.length !== 1) {
|
|
645
|
+
throw new SchemaLoadError("Exactly one input source must be provided: either file or url.");
|
|
646
|
+
}
|
|
647
|
+
if (input.file) {
|
|
648
|
+
return loadOpenApiDocumentFromFile(input.file, options.logger);
|
|
649
|
+
}
|
|
650
|
+
return loadOpenApiDocumentFromUrl(input.url, options.fetchImplementation, options.logger);
|
|
651
|
+
}
|
|
652
|
+
async function loadOpenApiDocumentFromFile(filePath, logger) {
|
|
653
|
+
try {
|
|
654
|
+
logger?.debug(`Loading OpenAPI schema from file: ${filePath}`);
|
|
655
|
+
const content = await (0, import_promises.readFile)(filePath, "utf8");
|
|
656
|
+
return parseSchema(content);
|
|
657
|
+
} catch (error) {
|
|
658
|
+
if (error instanceof ApiSdkGeneratorError) {
|
|
659
|
+
throw error;
|
|
660
|
+
}
|
|
661
|
+
throw new SchemaLoadError(
|
|
662
|
+
`Failed to load OpenAPI schema from file "${import_node_path.default.resolve(filePath)}".`,
|
|
663
|
+
error
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
async function loadOpenApiDocumentFromUrl(url, fetchImplementation, logger) {
|
|
668
|
+
const runtimeFetch = fetchImplementation ?? globalThis.fetch;
|
|
669
|
+
if (!runtimeFetch) {
|
|
670
|
+
throw new SchemaLoadError("Fetch API is not available in the current runtime.");
|
|
671
|
+
}
|
|
672
|
+
try {
|
|
673
|
+
logger?.debug(`Loading OpenAPI schema from URL: ${url}`);
|
|
674
|
+
const response = await runtimeFetch(url);
|
|
675
|
+
if (!response.ok) {
|
|
676
|
+
throw new SchemaLoadError(
|
|
677
|
+
`Failed to fetch OpenAPI schema from "${url}". HTTP ${response.status}.`
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
return parseSchema(await response.text());
|
|
681
|
+
} catch (error) {
|
|
682
|
+
if (error instanceof ApiSdkGeneratorError) {
|
|
683
|
+
throw error;
|
|
684
|
+
}
|
|
685
|
+
throw new SchemaLoadError(`Failed to load OpenAPI schema from URL "${url}".`, error);
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
// src/logger.ts
|
|
690
|
+
var NoopLogger = class {
|
|
691
|
+
info() {
|
|
692
|
+
}
|
|
693
|
+
warn() {
|
|
694
|
+
}
|
|
695
|
+
error() {
|
|
696
|
+
}
|
|
697
|
+
debug() {
|
|
698
|
+
}
|
|
699
|
+
};
|
|
700
|
+
var noopLogger = new NoopLogger();
|
|
701
|
+
function createLogger(options = {}) {
|
|
702
|
+
const sink = options.sink ?? console;
|
|
703
|
+
const verbose = options.verbose ?? false;
|
|
704
|
+
return {
|
|
705
|
+
debug(message) {
|
|
706
|
+
if (verbose) {
|
|
707
|
+
sink.info(message);
|
|
708
|
+
}
|
|
709
|
+
},
|
|
710
|
+
error(message) {
|
|
711
|
+
sink.error(message);
|
|
712
|
+
},
|
|
713
|
+
info(message) {
|
|
714
|
+
sink.info(message);
|
|
715
|
+
},
|
|
716
|
+
warn(message) {
|
|
717
|
+
sink.warn(message);
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/parser.ts
|
|
723
|
+
var SUPPORTED_METHODS = [
|
|
724
|
+
"get",
|
|
725
|
+
"post",
|
|
726
|
+
"put",
|
|
727
|
+
"patch",
|
|
728
|
+
"delete",
|
|
729
|
+
"head",
|
|
730
|
+
"options"
|
|
731
|
+
];
|
|
732
|
+
function getParameterSchema(document, parameter) {
|
|
733
|
+
if (!parameter.schema) {
|
|
734
|
+
throw new UnsupportedSchemaError(
|
|
735
|
+
`Parameter "${parameter.name}" in "${parameter.in}" is missing schema definition.`
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
resolveSchema(document, parameter.schema);
|
|
739
|
+
return parameter.schema;
|
|
740
|
+
}
|
|
741
|
+
function dereferenceParameter(document, parameter) {
|
|
742
|
+
if (!("$ref" in parameter)) {
|
|
743
|
+
return parameter;
|
|
744
|
+
}
|
|
745
|
+
return resolveLocalComponent(parameter, document.components?.parameters, "parameters");
|
|
746
|
+
}
|
|
747
|
+
function extractParameters(document, parameters) {
|
|
748
|
+
return (parameters ?? []).map((parameter) => dereferenceParameter(document, parameter)).filter((parameter) => parameter.in === "path" || parameter.in === "query").map((parameter) => ({
|
|
749
|
+
description: parameter.description,
|
|
750
|
+
explode: parameter.explode,
|
|
751
|
+
style: parameter.style,
|
|
752
|
+
in: parameter.in === "path" ? "path" : "query",
|
|
753
|
+
name: parameter.name,
|
|
754
|
+
required: parameter.in === "path" ? true : parameter.required ?? false,
|
|
755
|
+
schema: getParameterSchema(document, parameter)
|
|
756
|
+
}));
|
|
757
|
+
}
|
|
758
|
+
function mergeParameters(document, pathParameters, operationParameters) {
|
|
759
|
+
const merged = /* @__PURE__ */ new Map();
|
|
760
|
+
for (const parameter of extractParameters(document, pathParameters)) {
|
|
761
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
762
|
+
}
|
|
763
|
+
for (const parameter of extractParameters(document, operationParameters)) {
|
|
764
|
+
merged.set(`${parameter.in}:${parameter.name}`, parameter);
|
|
765
|
+
}
|
|
766
|
+
return [...merged.values()];
|
|
767
|
+
}
|
|
768
|
+
function getJsonContent(container) {
|
|
769
|
+
const content = container.content ?? {};
|
|
770
|
+
const entries = Object.entries(content);
|
|
771
|
+
for (const [contentType, value] of entries) {
|
|
772
|
+
if (contentType === "application/json" || contentType.endsWith("+json")) {
|
|
773
|
+
return { contentType, value };
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
return null;
|
|
777
|
+
}
|
|
778
|
+
function extractRequestBody(document, requestBody) {
|
|
779
|
+
if (!requestBody) {
|
|
780
|
+
return void 0;
|
|
781
|
+
}
|
|
782
|
+
const resolved = "$ref" in requestBody ? resolveRequestBody(document, requestBody) : requestBody;
|
|
783
|
+
const jsonContent = getJsonContent(resolved);
|
|
784
|
+
if (!jsonContent) {
|
|
785
|
+
throw new UnsupportedSchemaError("Only JSON request bodies are supported.");
|
|
786
|
+
}
|
|
787
|
+
const schema = jsonContent.value.schema;
|
|
788
|
+
if (!schema) {
|
|
789
|
+
throw new UnsupportedSchemaError("JSON request body must include a schema.");
|
|
790
|
+
}
|
|
791
|
+
return {
|
|
792
|
+
contentType: jsonContent.contentType,
|
|
793
|
+
required: resolved.required ?? false,
|
|
794
|
+
schema
|
|
795
|
+
};
|
|
796
|
+
}
|
|
797
|
+
function resolveRequestBody(document, requestBody) {
|
|
798
|
+
return resolveLocalComponent(requestBody, document.components?.requestBodies, "requestBodies");
|
|
799
|
+
}
|
|
800
|
+
function extractResponse(operation) {
|
|
801
|
+
const entries = Object.entries(operation.responses ?? {});
|
|
802
|
+
const successfulEntry = entries.find(([status]) => /^2(?:\d\d|XX)$/i.test(status)) ?? entries[0];
|
|
803
|
+
if (!successfulEntry) {
|
|
804
|
+
return {
|
|
805
|
+
response: {
|
|
806
|
+
description: "No content"
|
|
807
|
+
},
|
|
808
|
+
statusCode: "204"
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
return {
|
|
812
|
+
response: successfulEntry[1],
|
|
813
|
+
statusCode: successfulEntry[0]
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
function resolveResponse(document, response) {
|
|
817
|
+
if (!("$ref" in response)) {
|
|
818
|
+
return response;
|
|
819
|
+
}
|
|
820
|
+
return resolveLocalComponent(response, document.components?.responses, "responses");
|
|
821
|
+
}
|
|
822
|
+
function parseResponse(document, operation) {
|
|
823
|
+
const { response, statusCode } = extractResponse(operation);
|
|
824
|
+
const resolved = resolveResponse(document, response);
|
|
825
|
+
const jsonContent = getJsonContent(resolved);
|
|
826
|
+
return {
|
|
827
|
+
contentType: jsonContent?.contentType,
|
|
828
|
+
description: resolved.description,
|
|
829
|
+
schema: jsonContent?.value.schema,
|
|
830
|
+
statusCode
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
function hasRequiredRequestFields(parameters, requestBody) {
|
|
834
|
+
return parameters.some((parameter) => parameter.required) || Boolean(requestBody?.required);
|
|
835
|
+
}
|
|
836
|
+
function parseDocument(document, options = {}) {
|
|
837
|
+
const operations = [];
|
|
838
|
+
const functionNames = /* @__PURE__ */ new Set();
|
|
839
|
+
const typeNames = /* @__PURE__ */ new Set([
|
|
840
|
+
"ClientConfig",
|
|
841
|
+
"ApiError",
|
|
842
|
+
`${deriveSdkName(options.sdkName ?? document.info.title)}Client`
|
|
843
|
+
]);
|
|
844
|
+
for (const name of Object.keys(document.components?.schemas ?? {})) {
|
|
845
|
+
const typeName = toTypeName(name);
|
|
846
|
+
if (typeNames.has(typeName)) {
|
|
847
|
+
throw new SchemaValidationError(`Duplicate or reserved generated type name "${typeName}".`);
|
|
848
|
+
}
|
|
849
|
+
typeNames.add(typeName);
|
|
850
|
+
}
|
|
851
|
+
for (const [pathKey, pathItem] of Object.entries(document.paths ?? {})) {
|
|
852
|
+
if (!pathItem || "$ref" in pathItem) {
|
|
853
|
+
throw new UnsupportedSchemaError(`Path-level $ref is not supported for path "${pathKey}".`);
|
|
854
|
+
}
|
|
855
|
+
if (pathItem.trace) {
|
|
856
|
+
throw new UnsupportedSchemaError("TRACE operations are not supported by the Fetch API.");
|
|
857
|
+
}
|
|
858
|
+
for (const method of SUPPORTED_METHODS) {
|
|
859
|
+
const operation = pathItem[method];
|
|
860
|
+
if (!operation) {
|
|
861
|
+
continue;
|
|
862
|
+
}
|
|
863
|
+
const functionName = createFunctionName(method, pathKey, operation.operationId);
|
|
864
|
+
if (functionNames.has(functionName)) {
|
|
865
|
+
throw new SchemaValidationError(
|
|
866
|
+
`Duplicate generated operation name "${functionName}". Use unique operationId values.`
|
|
867
|
+
);
|
|
868
|
+
}
|
|
869
|
+
functionNames.add(functionName);
|
|
870
|
+
const allocateTypeName = (base) => {
|
|
871
|
+
let name = base;
|
|
872
|
+
let suffix = 2;
|
|
873
|
+
while (typeNames.has(name)) name = `${base}${suffix++}`;
|
|
874
|
+
typeNames.add(name);
|
|
875
|
+
return name;
|
|
876
|
+
};
|
|
877
|
+
const requestTypeName = allocateTypeName(`${toTypeName(functionName)}Request`);
|
|
878
|
+
const responseTypeName = allocateTypeName(`${toTypeName(functionName)}Response`);
|
|
879
|
+
const mergedParameters = mergeParameters(document, pathItem.parameters, operation.parameters);
|
|
880
|
+
const queryParameters = mergedParameters.filter((parameter) => parameter.in === "query");
|
|
881
|
+
const extractedPathParameters = mergedParameters.filter(
|
|
882
|
+
(parameter) => parameter.in === "path"
|
|
883
|
+
);
|
|
884
|
+
const requestBody = extractRequestBody(document, operation.requestBody);
|
|
885
|
+
const response = parseResponse(document, operation);
|
|
886
|
+
const requestNames = /* @__PURE__ */ new Set();
|
|
887
|
+
for (const parameter of mergedParameters) {
|
|
888
|
+
if (requestNames.has(parameter.name) || parameter.name === "body" && requestBody) {
|
|
889
|
+
throw new UnsupportedSchemaError(
|
|
890
|
+
`Request field "${parameter.name}" collides in ${method.toUpperCase()} ${pathKey}.`
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
requestNames.add(parameter.name);
|
|
894
|
+
if (parameter.in === "query") {
|
|
895
|
+
const schema = resolveSchema(document, parameter.schema);
|
|
896
|
+
const style = parameter.style ?? "form";
|
|
897
|
+
if (schema.type === "object" || schema.properties || !["form", "spaceDelimited", "pipeDelimited"].includes(style)) {
|
|
898
|
+
throw new UnsupportedSchemaError(
|
|
899
|
+
`Unsupported query serialization for "${parameter.name}": ${style}.`
|
|
900
|
+
);
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
for (const match of pathKey.matchAll(/\{([^}]+)\}/g)) {
|
|
905
|
+
if (!extractedPathParameters.some((parameter) => parameter.name === match[1])) {
|
|
906
|
+
throw new SchemaValidationError(
|
|
907
|
+
`Missing path parameter "${match[1]}" in ${method.toUpperCase()} ${pathKey}.`
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const hasRequestShape = extractedPathParameters.length + queryParameters.length > 0 || Boolean(requestBody);
|
|
912
|
+
operations.push({
|
|
913
|
+
description: operation.description,
|
|
914
|
+
functionName,
|
|
915
|
+
hasRequiredRequestFields: hasRequiredRequestFields(mergedParameters, requestBody),
|
|
916
|
+
hasRequestShape,
|
|
917
|
+
method,
|
|
918
|
+
operationId: operation.operationId,
|
|
919
|
+
path: pathKey,
|
|
920
|
+
pathParameters: extractedPathParameters,
|
|
921
|
+
queryParameters,
|
|
922
|
+
requestBody,
|
|
923
|
+
requestTypeName,
|
|
924
|
+
response,
|
|
925
|
+
responseTypeName,
|
|
926
|
+
summary: operation.summary
|
|
927
|
+
});
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
const sdkName = deriveSdkName(options.sdkName ?? document.info.title);
|
|
931
|
+
const defaultBaseUrl = options.baseUrl ?? document.servers?.[0]?.url;
|
|
932
|
+
return {
|
|
933
|
+
componentSchemas: document.components?.schemas ?? {},
|
|
934
|
+
defaultBaseUrl,
|
|
935
|
+
document,
|
|
936
|
+
operations,
|
|
937
|
+
sdkName
|
|
938
|
+
};
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// src/writer.ts
|
|
942
|
+
var import_promises2 = require("fs/promises");
|
|
943
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
944
|
+
var import_node_path2 = __toESM(require("path"), 1);
|
|
945
|
+
function resolveGeneratedFiles(outputDir, files) {
|
|
946
|
+
const outputRoot = import_node_path2.default.resolve(outputDir);
|
|
947
|
+
const seenPaths = /* @__PURE__ */ new Set();
|
|
948
|
+
return files.map((file) => {
|
|
949
|
+
const fullPath = import_node_path2.default.resolve(outputRoot, file.path);
|
|
950
|
+
const relativePath = import_node_path2.default.relative(outputRoot, fullPath);
|
|
951
|
+
if (!relativePath || relativePath === ".." || relativePath.startsWith(`..${import_node_path2.default.sep}`) || import_node_path2.default.isAbsolute(relativePath)) {
|
|
952
|
+
throw new OutputWriteError(
|
|
953
|
+
`Generated file path "${file.path}" must stay within the output directory.`
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
if (seenPaths.has(fullPath)) {
|
|
957
|
+
throw new OutputWriteError(`Duplicate generated file path "${file.path}".`);
|
|
958
|
+
}
|
|
959
|
+
seenPaths.add(fullPath);
|
|
960
|
+
return { ...file, fullPath };
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
async function writeGeneratedFiles(outputDir, files, clean = false) {
|
|
964
|
+
try {
|
|
965
|
+
const resolvedFiles = resolveGeneratedFiles(outputDir, files);
|
|
966
|
+
if (clean) {
|
|
967
|
+
const outputRoot = import_node_path2.default.resolve(outputDir);
|
|
968
|
+
const relativeCwd = import_node_path2.default.relative(outputRoot, process.cwd());
|
|
969
|
+
if (outputRoot === import_node_os.default.homedir() || relativeCwd === "" || relativeCwd !== ".." && !relativeCwd.startsWith(`..${import_node_path2.default.sep}`) && !import_node_path2.default.isAbsolute(relativeCwd)) {
|
|
970
|
+
throw new OutputWriteError(
|
|
971
|
+
"Refusing to clean the home directory, working directory, or its ancestors."
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
await (0, import_promises2.rm)(outputDir, { force: true, recursive: true });
|
|
975
|
+
}
|
|
976
|
+
await (0, import_promises2.mkdir)(outputDir, { recursive: true });
|
|
977
|
+
await Promise.all(
|
|
978
|
+
resolvedFiles.map(async (file) => {
|
|
979
|
+
await (0, import_promises2.mkdir)(import_node_path2.default.dirname(file.fullPath), { recursive: true });
|
|
980
|
+
await (0, import_promises2.writeFile)(file.fullPath, file.content, "utf8");
|
|
981
|
+
})
|
|
982
|
+
);
|
|
983
|
+
} catch (error) {
|
|
984
|
+
if (error instanceof OutputWriteError) {
|
|
985
|
+
throw error;
|
|
986
|
+
}
|
|
987
|
+
throw new OutputWriteError(`Failed to write generated SDK to "${outputDir}".`, error);
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
async function compareGeneratedFiles(outputDir, files) {
|
|
991
|
+
const changedFiles = [];
|
|
992
|
+
for (const file of resolveGeneratedFiles(outputDir, files)) {
|
|
993
|
+
try {
|
|
994
|
+
if (await (0, import_promises2.readFile)(file.fullPath, "utf8") !== file.content) {
|
|
995
|
+
changedFiles.push(file.path);
|
|
996
|
+
}
|
|
997
|
+
} catch (error) {
|
|
998
|
+
if (error.code === "ENOENT") {
|
|
999
|
+
changedFiles.push(file.path);
|
|
1000
|
+
} else {
|
|
1001
|
+
throw new OutputWriteError(`Failed to compare generated file "${file.fullPath}".`, error);
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
return changedFiles;
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
// src/generate-sdk.ts
|
|
1009
|
+
async function generateSdk(options) {
|
|
1010
|
+
const logger = options.logger ?? noopLogger;
|
|
1011
|
+
if (options.check && options.dryRun) {
|
|
1012
|
+
throw new OutputWriteError("The check and dryRun options cannot be combined.");
|
|
1013
|
+
}
|
|
1014
|
+
if (options.clean && !options.check && !options.dryRun && options.input.file) {
|
|
1015
|
+
const relative = import_node_path3.default.relative(
|
|
1016
|
+
import_node_path3.default.resolve(options.outputDir),
|
|
1017
|
+
import_node_path3.default.resolve(options.input.file)
|
|
1018
|
+
);
|
|
1019
|
+
if (relative !== ".." && !relative.startsWith(`..${import_node_path3.default.sep}`) && !import_node_path3.default.isAbsolute(relative)) {
|
|
1020
|
+
throw new OutputWriteError("Cannot clean an output directory containing the input schema.");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
logger.info("Loading OpenAPI document");
|
|
1024
|
+
const document = await loadOpenApiDocument(options.input, {
|
|
1025
|
+
fetchImplementation: options.fetchImplementation,
|
|
1026
|
+
logger
|
|
1027
|
+
});
|
|
1028
|
+
logger.info("Parsing OpenAPI document");
|
|
1029
|
+
const parsed = parseDocument(document, {
|
|
1030
|
+
baseUrl: options.baseUrl,
|
|
1031
|
+
sdkName: options.sdkName
|
|
1032
|
+
});
|
|
1033
|
+
logger.info(`Generating SDK for ${parsed.operations.length} operations`);
|
|
1034
|
+
const files = await formatGeneratedFiles([
|
|
1035
|
+
{
|
|
1036
|
+
content: generateTypesSource(parsed),
|
|
1037
|
+
path: "types.ts"
|
|
1038
|
+
},
|
|
1039
|
+
{
|
|
1040
|
+
content: generateClientSource(parsed),
|
|
1041
|
+
path: "client.ts"
|
|
1042
|
+
},
|
|
1043
|
+
{
|
|
1044
|
+
content: generateIndexSource(),
|
|
1045
|
+
path: "index.ts"
|
|
1046
|
+
},
|
|
1047
|
+
{
|
|
1048
|
+
content: generateGeneratedReadme(parsed),
|
|
1049
|
+
path: "README.md"
|
|
1050
|
+
}
|
|
1051
|
+
]);
|
|
1052
|
+
const changedFiles = options.check ? await compareGeneratedFiles(options.outputDir, files) : [];
|
|
1053
|
+
if (!options.check && !options.dryRun) {
|
|
1054
|
+
logger.info(`Writing SDK to ${options.outputDir}`);
|
|
1055
|
+
await writeGeneratedFiles(options.outputDir, files, options.clean);
|
|
1056
|
+
}
|
|
1057
|
+
return {
|
|
1058
|
+
changedFiles,
|
|
1059
|
+
files,
|
|
1060
|
+
operations: parsed.operations.length,
|
|
1061
|
+
outputDir: options.outputDir,
|
|
1062
|
+
sdkName: parsed.sdkName
|
|
1063
|
+
};
|
|
1064
|
+
}
|
|
1065
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1066
|
+
0 && (module.exports = {
|
|
1067
|
+
ApiSdkGeneratorError,
|
|
1068
|
+
OutputWriteError,
|
|
1069
|
+
SchemaLoadError,
|
|
1070
|
+
SchemaValidationError,
|
|
1071
|
+
UnsupportedSchemaError,
|
|
1072
|
+
createFunctionName,
|
|
1073
|
+
createLogger,
|
|
1074
|
+
deriveSdkName,
|
|
1075
|
+
generateSdk,
|
|
1076
|
+
getReferenceName,
|
|
1077
|
+
isReferenceObject,
|
|
1078
|
+
isSchemaObject,
|
|
1079
|
+
loadOpenApiDocument,
|
|
1080
|
+
loadOpenApiDocumentFromFile,
|
|
1081
|
+
loadOpenApiDocumentFromUrl,
|
|
1082
|
+
noopLogger,
|
|
1083
|
+
parseDocument,
|
|
1084
|
+
resolveLocalComponent,
|
|
1085
|
+
resolveSchema,
|
|
1086
|
+
sanitizeIdentifier,
|
|
1087
|
+
toCamelCase,
|
|
1088
|
+
toPascalCase,
|
|
1089
|
+
toPropertyAccessor,
|
|
1090
|
+
toTypeName,
|
|
1091
|
+
validateOpenApiDocument
|
|
1092
|
+
});
|
|
1093
|
+
//# sourceMappingURL=index.cjs.map
|