oapiex 0.1.2 → 0.2.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/README.md +6 -5
- package/bin/cli.mjs +131 -6
- package/dist/index.cjs +2562 -391
- package/dist/index.d.ts +346 -30
- package/dist/index.mjs +2555 -370
- package/package.json +9 -6
package/dist/index.cjs
CHANGED
|
@@ -36,10 +36,11 @@ node_path = __toESM(node_path);
|
|
|
36
36
|
let node_fs_promises = require("node:fs/promises");
|
|
37
37
|
node_fs_promises = __toESM(node_fs_promises);
|
|
38
38
|
let _h3ravel_musket = require("@h3ravel/musket");
|
|
39
|
-
let url = require("url");
|
|
40
39
|
let prettier = require("prettier");
|
|
41
40
|
prettier = __toESM(prettier);
|
|
42
41
|
let node_url = require("node:url");
|
|
42
|
+
let node_fs = require("node:fs");
|
|
43
|
+
let url = require("url");
|
|
43
44
|
|
|
44
45
|
//#region src/Manager.ts
|
|
45
46
|
const supportedBrowsers = [
|
|
@@ -166,6 +167,7 @@ const browser = async (source, config = globalConfig, initial = false) => {
|
|
|
166
167
|
}
|
|
167
168
|
} else if (config.browser === "puppeteer") {
|
|
168
169
|
const activeSession = getBrowserSession();
|
|
170
|
+
const browserTimeout = Math.max(config.requestTimeout, 3e4);
|
|
169
171
|
let browserInstance = activeSession?.browser === "puppeteer" ? activeSession.puppeteerBrowser : void 0;
|
|
170
172
|
let shouldClose = false;
|
|
171
173
|
let page;
|
|
@@ -193,14 +195,12 @@ const browser = async (source, config = globalConfig, initial = false) => {
|
|
|
193
195
|
try {
|
|
194
196
|
await page.goto(source, {
|
|
195
197
|
waitUntil: "domcontentloaded",
|
|
196
|
-
timeout:
|
|
198
|
+
timeout: browserTimeout
|
|
197
199
|
});
|
|
198
200
|
} catch (error) {
|
|
199
201
|
if (!page || !await hasExtractableReadmeContent(page)) throw error;
|
|
200
202
|
}
|
|
201
|
-
await
|
|
202
|
-
await waitForOperationHydration(page, config.requestTimeout);
|
|
203
|
-
let html = await page.content();
|
|
203
|
+
let html = await extractStablePageHtml(page, browserTimeout, initial);
|
|
204
204
|
if (!html) throw new Error(`Unable to extract HTML from remote source: ${source}`);
|
|
205
205
|
if (!html.includes("id=\"ssr-props\"")) {
|
|
206
206
|
const { data: rawHtml } = await axios.default.get(source, {
|
|
@@ -248,6 +248,37 @@ const waitForOperationHydration = async (page, timeout) => {
|
|
|
248
248
|
});
|
|
249
249
|
} catch {}
|
|
250
250
|
};
|
|
251
|
+
const extractStablePageHtml = async (page, timeout, initial = false, maxAttempts = 3) => {
|
|
252
|
+
let lastError;
|
|
253
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) try {
|
|
254
|
+
await waitForExtractableReadmeContent(page, timeout, initial);
|
|
255
|
+
await waitForOperationHydration(page, timeout);
|
|
256
|
+
return await page.content();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
lastError = error;
|
|
259
|
+
if (!isExecutionContextNavigationError(error) || attempt === maxAttempts) throw error;
|
|
260
|
+
await waitForNavigationSettle(page, timeout);
|
|
261
|
+
}
|
|
262
|
+
throw lastError instanceof Error ? lastError : /* @__PURE__ */ new Error("Unable to extract stable HTML from remote source");
|
|
263
|
+
};
|
|
264
|
+
const waitForNavigationSettle = async (page, timeout) => {
|
|
265
|
+
try {
|
|
266
|
+
await page.waitForFunction(() => {
|
|
267
|
+
return document.readyState === "interactive" || document.readyState === "complete";
|
|
268
|
+
}, { timeout: Math.min(timeout, 5e3) });
|
|
269
|
+
} catch {}
|
|
270
|
+
try {
|
|
271
|
+
await page.waitForNetworkIdle?.({
|
|
272
|
+
idleTime: 500,
|
|
273
|
+
timeout: Math.min(timeout, 5e3)
|
|
274
|
+
});
|
|
275
|
+
} catch {}
|
|
276
|
+
};
|
|
277
|
+
const isExecutionContextNavigationError = (error) => {
|
|
278
|
+
if (!(error instanceof Error)) return false;
|
|
279
|
+
const message = error.message.toLowerCase();
|
|
280
|
+
return message.includes("execution context was destroyed") || message.includes("cannot find context with specified id") || message.includes("most likely because of a navigation");
|
|
281
|
+
};
|
|
251
282
|
const hasExtractableReadmeContent = async (page) => {
|
|
252
283
|
return Boolean(await page.$("[data-testid=\"http-method\"], article#content, script#ssr-props"));
|
|
253
284
|
};
|
|
@@ -263,41 +294,94 @@ const extractSsrPropsScript = (html) => {
|
|
|
263
294
|
return html.match(/<script id="ssr-props"[^>]*>[\s\S]*?<\/script>/i)?.[0] ?? null;
|
|
264
295
|
};
|
|
265
296
|
|
|
266
|
-
//#endregion
|
|
267
|
-
//#region src/Core.ts
|
|
268
|
-
const isRecord = (value) => {
|
|
269
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
270
|
-
};
|
|
271
|
-
|
|
272
297
|
//#endregion
|
|
273
298
|
//#region src/JsonRepair.ts
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
} catch {
|
|
280
|
-
const repaired = repairCommonJsonIssues(trimmed);
|
|
299
|
+
var JsonRepair = class JsonRepair {
|
|
300
|
+
static parsePossiblyTruncated = (value) => {
|
|
301
|
+
const repairer = new JsonRepair();
|
|
302
|
+
const trimmed = value.trim();
|
|
303
|
+
if (!/^(?:\{|\[)/.test(trimmed)) return null;
|
|
281
304
|
try {
|
|
282
|
-
return JSON.parse(
|
|
305
|
+
return JSON.parse(trimmed);
|
|
283
306
|
} catch {
|
|
284
|
-
|
|
307
|
+
const repaired = repairer.repairCommonJsonIssues(trimmed);
|
|
308
|
+
try {
|
|
309
|
+
return JSON.parse(repaired);
|
|
310
|
+
} catch {
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
285
313
|
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
314
|
+
};
|
|
315
|
+
repairCommonJsonIssues = (value) => {
|
|
316
|
+
const withUnexpectedTokensRemoved = this.removeUnexpectedObjectTokens(value);
|
|
317
|
+
const withMissingCommasInserted = this.insertMissingCommas(withUnexpectedTokensRemoved);
|
|
318
|
+
return `${withMissingCommasInserted}${this.buildMissingJsonClosers(withMissingCommasInserted)}`;
|
|
319
|
+
};
|
|
320
|
+
removeUnexpectedObjectTokens = (value) => {
|
|
321
|
+
return value.replace(/([[{,]\s*)([A-Za-z_$][\w$-]*)(?=\s*"(?:\\.|[^"\\])*"\s*:)/g, "$1");
|
|
322
|
+
};
|
|
323
|
+
insertMissingCommas = (value) => {
|
|
324
|
+
let result = "";
|
|
325
|
+
let inString = false;
|
|
326
|
+
let isEscaped = false;
|
|
327
|
+
let previousSignificantCharacter = "";
|
|
328
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
329
|
+
const character = value[index];
|
|
330
|
+
if (inString) {
|
|
331
|
+
result += character;
|
|
332
|
+
if (isEscaped) {
|
|
333
|
+
isEscaped = false;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (character === "\\") {
|
|
337
|
+
isEscaped = true;
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
if (character === "\"") {
|
|
341
|
+
inString = false;
|
|
342
|
+
previousSignificantCharacter = "\"";
|
|
343
|
+
}
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (character === "\"") {
|
|
347
|
+
const remainder = value.slice(index);
|
|
348
|
+
if (/^"(?:\\.|[^"\\])*"\s*:/.test(remainder) && this.shouldInsertCommaBeforeKey(previousSignificantCharacter, result)) result += ",";
|
|
349
|
+
result += character;
|
|
350
|
+
inString = true;
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
300
353
|
result += character;
|
|
354
|
+
if (!/\s/.test(character)) previousSignificantCharacter = character;
|
|
355
|
+
}
|
|
356
|
+
return result;
|
|
357
|
+
};
|
|
358
|
+
shouldInsertCommaBeforeKey = (previousSignificantCharacter, currentOutput) => {
|
|
359
|
+
if (!previousSignificantCharacter) return false;
|
|
360
|
+
if (![
|
|
361
|
+
"\"",
|
|
362
|
+
"}",
|
|
363
|
+
"]",
|
|
364
|
+
"e",
|
|
365
|
+
"l",
|
|
366
|
+
"0",
|
|
367
|
+
"1",
|
|
368
|
+
"2",
|
|
369
|
+
"3",
|
|
370
|
+
"4",
|
|
371
|
+
"5",
|
|
372
|
+
"6",
|
|
373
|
+
"7",
|
|
374
|
+
"8",
|
|
375
|
+
"9"
|
|
376
|
+
].includes(previousSignificantCharacter)) return false;
|
|
377
|
+
const trimmedOutput = currentOutput.trimEnd();
|
|
378
|
+
return !trimmedOutput.endsWith(",") && !trimmedOutput.endsWith("{");
|
|
379
|
+
};
|
|
380
|
+
buildMissingJsonClosers = (value) => {
|
|
381
|
+
const stack = [];
|
|
382
|
+
let inString = false;
|
|
383
|
+
let isEscaped = false;
|
|
384
|
+
for (const character of value) {
|
|
301
385
|
if (isEscaped) {
|
|
302
386
|
isEscaped = false;
|
|
303
387
|
continue;
|
|
@@ -307,74 +391,28 @@ const insertMissingCommas = (value) => {
|
|
|
307
391
|
continue;
|
|
308
392
|
}
|
|
309
393
|
if (character === "\"") {
|
|
310
|
-
inString =
|
|
311
|
-
|
|
394
|
+
inString = !inString;
|
|
395
|
+
continue;
|
|
312
396
|
}
|
|
313
|
-
continue;
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
397
|
+
if (inString) continue;
|
|
398
|
+
if (character === "{") {
|
|
399
|
+
stack.push("}");
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (character === "[") {
|
|
403
|
+
stack.push("]");
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
if ((character === "}" || character === "]") && stack[stack.length - 1] === character) stack.pop();
|
|
321
407
|
}
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
}
|
|
325
|
-
return result;
|
|
408
|
+
return stack.reverse().join("");
|
|
409
|
+
};
|
|
326
410
|
};
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
"]",
|
|
333
|
-
"e",
|
|
334
|
-
"l",
|
|
335
|
-
"0",
|
|
336
|
-
"1",
|
|
337
|
-
"2",
|
|
338
|
-
"3",
|
|
339
|
-
"4",
|
|
340
|
-
"5",
|
|
341
|
-
"6",
|
|
342
|
-
"7",
|
|
343
|
-
"8",
|
|
344
|
-
"9"
|
|
345
|
-
].includes(previousSignificantCharacter)) return false;
|
|
346
|
-
const trimmedOutput = currentOutput.trimEnd();
|
|
347
|
-
return !trimmedOutput.endsWith(",") && !trimmedOutput.endsWith("{");
|
|
348
|
-
};
|
|
349
|
-
const buildMissingJsonClosers = (value) => {
|
|
350
|
-
const stack = [];
|
|
351
|
-
let inString = false;
|
|
352
|
-
let isEscaped = false;
|
|
353
|
-
for (const character of value) {
|
|
354
|
-
if (isEscaped) {
|
|
355
|
-
isEscaped = false;
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
if (character === "\\") {
|
|
359
|
-
isEscaped = true;
|
|
360
|
-
continue;
|
|
361
|
-
}
|
|
362
|
-
if (character === "\"") {
|
|
363
|
-
inString = !inString;
|
|
364
|
-
continue;
|
|
365
|
-
}
|
|
366
|
-
if (inString) continue;
|
|
367
|
-
if (character === "{") {
|
|
368
|
-
stack.push("}");
|
|
369
|
-
continue;
|
|
370
|
-
}
|
|
371
|
-
if (character === "[") {
|
|
372
|
-
stack.push("]");
|
|
373
|
-
continue;
|
|
374
|
-
}
|
|
375
|
-
if ((character === "}" || character === "]") && stack[stack.length - 1] === character) stack.pop();
|
|
376
|
-
}
|
|
377
|
-
return stack.reverse().join("");
|
|
411
|
+
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/Core.ts
|
|
414
|
+
const isRecord = (value) => {
|
|
415
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
378
416
|
};
|
|
379
417
|
|
|
380
418
|
//#endregion
|
|
@@ -717,11 +755,16 @@ const extractParameterDescription = (param) => {
|
|
|
717
755
|
const normalizeResponseBody = (body, contentType) => {
|
|
718
756
|
const trimmed = body.trim();
|
|
719
757
|
if (contentType?.toLowerCase().includes("json") || /^(?:\{|\[)/.test(trimmed)) {
|
|
720
|
-
const parsedBody =
|
|
758
|
+
const parsedBody = JsonRepair.parsePossiblyTruncated(trimmed);
|
|
721
759
|
if (parsedBody !== null) return {
|
|
722
760
|
format: "json",
|
|
723
761
|
body: parsedBody
|
|
724
762
|
};
|
|
763
|
+
const looseParsedBody = parseLooseStructuredValue(trimmed);
|
|
764
|
+
if (looseParsedBody !== null) return {
|
|
765
|
+
format: "json",
|
|
766
|
+
body: looseParsedBody
|
|
767
|
+
};
|
|
725
768
|
return {
|
|
726
769
|
format: "text",
|
|
727
770
|
body
|
|
@@ -869,15 +912,114 @@ const extractStringLiteralValue = (source, startIndex) => {
|
|
|
869
912
|
return null;
|
|
870
913
|
};
|
|
871
914
|
const parseLooseStructuredValue = (value) => {
|
|
872
|
-
const trimmed = value.trim();
|
|
915
|
+
const trimmed = preprocessLooseStructuredValue(value).trim();
|
|
873
916
|
if (!/^[[{]/.test(trimmed)) return null;
|
|
874
|
-
const normalized = trimmed.replace(/([{,]\s*)([A-Za-z_$][\w$-]*)(\s*:)/g, "$1\"$2\"$3").replace(
|
|
917
|
+
const normalized = replaceSingleQuotedStringsOutsideDoubleQuotes(trimmed.replace(/([{,]\s*)([A-Za-z_$][\w$-]*)(\s*:)/g, "$1\"$2\"$3").replace(/,\s*([}\]])/g, "$1"));
|
|
875
918
|
try {
|
|
876
919
|
return JSON.parse(normalized);
|
|
877
920
|
} catch {
|
|
878
921
|
return null;
|
|
879
922
|
}
|
|
880
923
|
};
|
|
924
|
+
const replaceSingleQuotedStringsOutsideDoubleQuotes = (value) => {
|
|
925
|
+
let result = "";
|
|
926
|
+
let inDoubleString = false;
|
|
927
|
+
let inSingleString = false;
|
|
928
|
+
let isEscaped = false;
|
|
929
|
+
let singleQuotedContent = "";
|
|
930
|
+
for (const character of value) {
|
|
931
|
+
if (inSingleString) {
|
|
932
|
+
if (isEscaped) {
|
|
933
|
+
singleQuotedContent += character;
|
|
934
|
+
isEscaped = false;
|
|
935
|
+
continue;
|
|
936
|
+
}
|
|
937
|
+
if (character === "\\") {
|
|
938
|
+
isEscaped = true;
|
|
939
|
+
continue;
|
|
940
|
+
}
|
|
941
|
+
if (character === "'") {
|
|
942
|
+
result += JSON.stringify(singleQuotedContent);
|
|
943
|
+
singleQuotedContent = "";
|
|
944
|
+
inSingleString = false;
|
|
945
|
+
continue;
|
|
946
|
+
}
|
|
947
|
+
singleQuotedContent += character;
|
|
948
|
+
continue;
|
|
949
|
+
}
|
|
950
|
+
if (inDoubleString) {
|
|
951
|
+
result += character;
|
|
952
|
+
if (isEscaped) {
|
|
953
|
+
isEscaped = false;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (character === "\\") {
|
|
957
|
+
isEscaped = true;
|
|
958
|
+
continue;
|
|
959
|
+
}
|
|
960
|
+
if (character === "\"") inDoubleString = false;
|
|
961
|
+
continue;
|
|
962
|
+
}
|
|
963
|
+
if (character === "'") {
|
|
964
|
+
inSingleString = true;
|
|
965
|
+
singleQuotedContent = "";
|
|
966
|
+
continue;
|
|
967
|
+
}
|
|
968
|
+
if (character === "\"") inDoubleString = true;
|
|
969
|
+
result += character;
|
|
970
|
+
}
|
|
971
|
+
return result;
|
|
972
|
+
};
|
|
973
|
+
const preprocessLooseStructuredValue = (value) => {
|
|
974
|
+
let normalized = stripLineCommentsOutsideStrings(value);
|
|
975
|
+
normalized = removeLooseBareTokensBeforeKeys(normalized);
|
|
976
|
+
normalized = repairAnonymousDataEnvelope(normalized);
|
|
977
|
+
return normalized;
|
|
978
|
+
};
|
|
979
|
+
const stripLineCommentsOutsideStrings = (value) => {
|
|
980
|
+
let result = "";
|
|
981
|
+
let inString = false;
|
|
982
|
+
let isEscaped = false;
|
|
983
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
984
|
+
const character = value[index];
|
|
985
|
+
const nextCharacter = value[index + 1];
|
|
986
|
+
if (inString) {
|
|
987
|
+
result += character;
|
|
988
|
+
if (isEscaped) {
|
|
989
|
+
isEscaped = false;
|
|
990
|
+
continue;
|
|
991
|
+
}
|
|
992
|
+
if (character === "\\") {
|
|
993
|
+
isEscaped = true;
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
if (character === "\"") inString = false;
|
|
997
|
+
continue;
|
|
998
|
+
}
|
|
999
|
+
if (character === "\"") {
|
|
1000
|
+
inString = true;
|
|
1001
|
+
result += character;
|
|
1002
|
+
continue;
|
|
1003
|
+
}
|
|
1004
|
+
if (character === "/" && nextCharacter === "/") {
|
|
1005
|
+
while (index < value.length && value[index] !== "\n") index += 1;
|
|
1006
|
+
if (index < value.length) result += value[index];
|
|
1007
|
+
continue;
|
|
1008
|
+
}
|
|
1009
|
+
result += character;
|
|
1010
|
+
}
|
|
1011
|
+
return result;
|
|
1012
|
+
};
|
|
1013
|
+
const removeLooseBareTokensBeforeKeys = (value) => {
|
|
1014
|
+
return value.replace(/([[{]\s*)([A-Za-z_$][\w$-]*)(\s+)(?=")/g, "$1");
|
|
1015
|
+
};
|
|
1016
|
+
const repairAnonymousDataEnvelope = (value) => {
|
|
1017
|
+
const trimmed = value.trimStart();
|
|
1018
|
+
if (!trimmed.startsWith("{") || /"data"\s*:\s*\[/.test(trimmed)) return value;
|
|
1019
|
+
if (!/^\{\s*"[^"]+"\s*:/.test(trimmed)) return value;
|
|
1020
|
+
if (!/\]\s*,\s*"meta"\s*:/.test(trimmed)) return value;
|
|
1021
|
+
return `${value.slice(0, value.indexOf("{"))}{"data": [{${trimmed.slice(1)}`;
|
|
1022
|
+
};
|
|
881
1023
|
const escapeSelector = (value) => {
|
|
882
1024
|
return value.replace(/([#.:[\],=])/g, "\\$1");
|
|
883
1025
|
};
|
|
@@ -965,264 +1107,2306 @@ var Application = class {
|
|
|
965
1107
|
};
|
|
966
1108
|
|
|
967
1109
|
//#endregion
|
|
968
|
-
//#region src/
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
process.exit(1);
|
|
985
|
-
}
|
|
986
|
-
} catch {}
|
|
987
|
-
await node_fs_promises.default.writeFile(configPath, configTemplate, "utf8");
|
|
988
|
-
this.line(`Created ${configPath} `);
|
|
1110
|
+
//#region src/generator/TypeScriptModuleRenderer.ts
|
|
1111
|
+
var TypeScriptModuleRenderer = class {
|
|
1112
|
+
/**
|
|
1113
|
+
* Render a TypeScript declaration (interface, type alias, or shape alias) into its
|
|
1114
|
+
* string representation.
|
|
1115
|
+
*
|
|
1116
|
+
* @param declaration
|
|
1117
|
+
* @returns
|
|
1118
|
+
*/
|
|
1119
|
+
renderDeclaration(declaration) {
|
|
1120
|
+
switch (declaration.kind) {
|
|
1121
|
+
case "interface": return this.renderInterface(declaration);
|
|
1122
|
+
case "interface-alias": return `export interface ${declaration.name} extends ${declaration.target} {}`;
|
|
1123
|
+
case "type-alias": return `export type ${declaration.name} = ${declaration.target}`;
|
|
1124
|
+
case "shape-alias": return `export type ${declaration.name} = ${this.renderShape(declaration.shape)}`;
|
|
1125
|
+
}
|
|
989
1126
|
}
|
|
990
|
-
|
|
991
|
-
|
|
1127
|
+
/**
|
|
1128
|
+
* Render the TypeScript type definitions for an OpenAPI document, including the main
|
|
1129
|
+
* document structure and the individual operation definitions, using the provided type
|
|
1130
|
+
* references for each operation.
|
|
1131
|
+
*
|
|
1132
|
+
* @param rootTypeName
|
|
1133
|
+
* @param document
|
|
1134
|
+
* @param operationTypeRefs
|
|
1135
|
+
* @returns
|
|
1136
|
+
*/
|
|
1137
|
+
renderOpenApiDocumentDefinitions(rootTypeName, document, operationTypeRefs) {
|
|
992
1138
|
return [
|
|
993
|
-
|
|
994
|
-
"",
|
|
995
|
-
"
|
|
996
|
-
"
|
|
997
|
-
"
|
|
998
|
-
"export
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1139
|
+
"export interface OpenApiInfo {\n title: string\n version: string\n}",
|
|
1140
|
+
"export interface OpenApiSchemaDefinition {\n type?: string\n description?: string\n default?: unknown\n properties?: Record<string, OpenApiSchemaDefinition>\n items?: OpenApiSchemaDefinition\n required?: string[]\n example?: unknown\n}",
|
|
1141
|
+
"export interface OpenApiParameterDefinition {\n name: string\n in: 'query' | 'header' | 'path' | 'cookie'\n required?: boolean\n description?: string\n schema?: OpenApiSchemaDefinition\n example?: unknown\n}",
|
|
1142
|
+
"export interface OpenApiMediaTypeDefinition<TExample = unknown> {\n schema?: OpenApiSchemaDefinition\n example?: TExample\n}",
|
|
1143
|
+
"export interface OpenApiResponseDefinition<_TResponse = unknown, TExample = unknown> {\n description: string\n content?: Record<string, OpenApiMediaTypeDefinition<TExample>>\n}",
|
|
1144
|
+
"export interface OpenApiRequestBodyDefinition<TInput = unknown> {\n description?: string\n required: boolean\n content: Record<string, OpenApiMediaTypeDefinition<TInput>>\n}",
|
|
1145
|
+
"export interface OpenApiOperationDefinition<_TResponse = unknown, TResponseExample = unknown, TInput = Record<string, never>, _TQuery = Record<string, never>, _THeader = Record<string, never>, _TParams = Record<string, never>> {\n summary?: string\n description?: string\n operationId?: string\n parameters?: OpenApiParameterDefinition[]\n requestBody?: OpenApiRequestBodyDefinition<TInput>\n responses: Record<string, OpenApiResponseDefinition<_TResponse, TResponseExample>>\n}",
|
|
1146
|
+
"export interface OpenApiSdkParameterManifest {\n name: string\n accessor: string\n in: 'query' | 'header' | 'path'\n required: boolean\n description?: string\n}",
|
|
1147
|
+
"export interface OpenApiSdkOperationManifest {\n path: string\n method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'\n methodName: string\n summary?: string\n description?: string\n operationId?: string\n requestBodyDescription?: string\n responseDescription?: string\n responseType: string\n inputType: string\n queryType: string\n headerType: string\n paramsType: string\n hasBody: boolean\n bodyRequired: boolean\n pathParams: OpenApiSdkParameterManifest[]\n queryParams: OpenApiSdkParameterManifest[]\n headerParams: OpenApiSdkParameterManifest[]\n}",
|
|
1148
|
+
"export interface OpenApiSdkGroupManifest {\n className: string\n propertyName: string\n operations: OpenApiSdkOperationManifest[]\n}",
|
|
1149
|
+
"export interface OpenApiSdkManifest {\n groups: OpenApiSdkGroupManifest[]\n}",
|
|
1150
|
+
"export interface OpenApiRuntimeBundle<TApi = unknown> {\n document: unknown\n manifest: OpenApiSdkManifest\n __api?: TApi\n}",
|
|
1151
|
+
Object.entries(document.paths).map(([path, operations]) => {
|
|
1152
|
+
const pathTypeName = this.derivePathTypeName(path);
|
|
1153
|
+
return [Object.keys(operations).map((method) => {
|
|
1154
|
+
const operationTypeName = this.deriveOperationInterfaceName(path, method);
|
|
1155
|
+
const refs = operationTypeRefs.get(`${path}::${method}`) ?? {
|
|
1156
|
+
response: "Record<string, never>",
|
|
1157
|
+
responseExample: "unknown",
|
|
1158
|
+
input: "Record<string, never>",
|
|
1159
|
+
query: "Record<string, never>",
|
|
1160
|
+
header: "Record<string, never>",
|
|
1161
|
+
params: "Record<string, never>"
|
|
1162
|
+
};
|
|
1163
|
+
return `export interface ${operationTypeName} extends OpenApiOperationDefinition<${refs.response}, ${refs.responseExample}, ${refs.input}, ${refs.query}, ${refs.header}, ${refs.params}> {}`;
|
|
1164
|
+
}).join("\n\n"), `export interface ${pathTypeName} {\n${Object.keys(operations).map((method) => ` ${method}: ${this.deriveOperationInterfaceName(path, method)}`).join("\n")}\n}`].join("\n\n");
|
|
1165
|
+
}).join("\n\n"),
|
|
1166
|
+
`export interface Paths {\n${Object.keys(document.paths).map((path) => ` ${this.formatPropertyKey(path)}: ${this.derivePathTypeName(path)}`).join("\n")}\n}`,
|
|
1167
|
+
`export interface ${rootTypeName} {\n openapi: '3.1.0'\n info: OpenApiInfo\n paths: Paths\n}`
|
|
1168
|
+
].join("\n\n");
|
|
1169
|
+
}
|
|
1170
|
+
renderSdkApiInterface(rootTypeName, manifest) {
|
|
1171
|
+
return `export interface ${rootTypeName}Api {\n${manifest.groups.map((group) => {
|
|
1172
|
+
const methods = group.operations.map((operation) => ` ${operation.methodName}${this.renderSdkMethodSignature(operation)}`).join("\n");
|
|
1173
|
+
return ` ${group.propertyName}: {\n${methods}\n }`;
|
|
1174
|
+
}).join("\n")}\n}`;
|
|
1175
|
+
}
|
|
1176
|
+
renderSdkManifest(variableName, manifest) {
|
|
1177
|
+
return `export const ${variableName}Manifest = ${this.renderValue(manifest)} as const satisfies OpenApiSdkManifest`;
|
|
1178
|
+
}
|
|
1179
|
+
renderSdkBundle(variableName, rootTypeName) {
|
|
1180
|
+
return [
|
|
1181
|
+
`export const ${variableName}Sdk: OpenApiRuntimeBundle<${rootTypeName}Api> = {`,
|
|
1182
|
+
` document: ${variableName},`,
|
|
1183
|
+
` manifest: ${variableName}Manifest,`,
|
|
1184
|
+
"}"
|
|
1008
1185
|
].join("\n");
|
|
1009
1186
|
}
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
paths[normalized.path] ??= {};
|
|
1020
|
-
paths[normalized.path][normalized.method] = normalized.operation;
|
|
1187
|
+
/**
|
|
1188
|
+
* Render a value into a string representation suitable for inclusion in TypeScript
|
|
1189
|
+
* type definitions,
|
|
1190
|
+
*
|
|
1191
|
+
* @param value
|
|
1192
|
+
* @returns
|
|
1193
|
+
*/
|
|
1194
|
+
renderValue(value) {
|
|
1195
|
+
return this.renderLiteral(value, 0);
|
|
1021
1196
|
}
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
description: operation.description ?? void 0,
|
|
1046
|
-
operationId: buildOperationId(method, path),
|
|
1047
|
-
parameters: createParameters(operation.requestParams),
|
|
1048
|
-
requestBody: createRequestBody(operation.requestParams, operation.requestExampleNormalized?.body, hasExtractedBodyParams(operation.requestParams) ? null : resolveFallbackRequestBodyExample(operation)),
|
|
1049
|
-
responses: createResponses(operation.responseSchemas, operation.responseBodies)
|
|
1197
|
+
renderOpenApiDocumentValue(document) {
|
|
1198
|
+
return this.renderLiteral(this.normalizeOpenApiDocument(document), 0);
|
|
1199
|
+
}
|
|
1200
|
+
/**
|
|
1201
|
+
* Convert a string to camelCase, sanitizing it to create a valid TypeScript identifier.
|
|
1202
|
+
*
|
|
1203
|
+
* @param value
|
|
1204
|
+
* @returns
|
|
1205
|
+
*/
|
|
1206
|
+
toCamelCase(value) {
|
|
1207
|
+
const typeName = this.sanitizeTypeName(value);
|
|
1208
|
+
return typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
1209
|
+
}
|
|
1210
|
+
renderInterface(declaration) {
|
|
1211
|
+
const body = declaration.properties.map((property) => ` ${this.formatPropertyKey(property.key)}${property.optional ? "?" : ""}: ${this.renderShape(property.shape)}`).join("\n");
|
|
1212
|
+
return `export interface ${declaration.name} {\n${body}\n}`;
|
|
1213
|
+
}
|
|
1214
|
+
renderShape(shape) {
|
|
1215
|
+
switch (shape.kind) {
|
|
1216
|
+
case "primitive": return shape.type;
|
|
1217
|
+
case "array": return `${this.wrapUnion(this.renderShape(shape.item))}[]`;
|
|
1218
|
+
case "union": return shape.types.map((entry) => this.renderShape(entry)).join(" | ");
|
|
1219
|
+
case "object": return this.inlineObjectShape(shape);
|
|
1050
1220
|
}
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1221
|
+
}
|
|
1222
|
+
inlineObjectShape(shape) {
|
|
1223
|
+
if (shape.properties.length === 0) return "Record<string, never>";
|
|
1224
|
+
return `{ ${shape.properties.map((property) => `${this.formatPropertyKey(property.key)}${property.optional ? "?" : ""}: ${this.renderShape(property.shape)}`).join("; ")} }`;
|
|
1225
|
+
}
|
|
1226
|
+
wrapUnion(value) {
|
|
1227
|
+
return value.includes(" | ") ? `(${value})` : value;
|
|
1228
|
+
}
|
|
1229
|
+
derivePathTypeName(path) {
|
|
1230
|
+
const segments = path.split("/").map((segment) => segment.trim()).filter(Boolean).filter((segment) => !/^v\d+$/i.test(segment)).map((segment) => this.isPathParam(segment) ? `by ${this.stripPathParam(segment)}` : segment);
|
|
1231
|
+
return `${this.sanitizeTypeName(segments.join(" "))}Path`;
|
|
1232
|
+
}
|
|
1233
|
+
deriveOperationInterfaceName(path, method) {
|
|
1234
|
+
return `${this.derivePathTypeName(path)}${this.sanitizeTypeName(method)}Operation`;
|
|
1235
|
+
}
|
|
1236
|
+
sanitizeTypeName(value) {
|
|
1237
|
+
const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
1238
|
+
if (!normalized) return "GeneratedEntity";
|
|
1239
|
+
const pascalCased = normalized.split(/\s+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
|
|
1240
|
+
return /^[A-Za-z_$]/.test(pascalCased) ? pascalCased : `Type${pascalCased}`;
|
|
1241
|
+
}
|
|
1242
|
+
formatPropertyKey(key) {
|
|
1243
|
+
return /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(key) ? key : `'${this.escapeStringLiteral(key)}'`;
|
|
1244
|
+
}
|
|
1245
|
+
renderSdkMethodSignature(operation) {
|
|
1246
|
+
const args = [];
|
|
1247
|
+
if (operation.pathParams.length > 0) args.push(`(params: ${operation.paramsType}`);
|
|
1248
|
+
if (operation.queryParams.length > 0) args.push(`${args.length === 0 ? "(" : ", "}query: ${operation.queryType}`);
|
|
1249
|
+
if (operation.hasBody) args.push(`${args.length === 0 ? "(" : ", "}body${operation.bodyRequired ? "" : "?"}: ${operation.inputType}`);
|
|
1250
|
+
if (operation.headerParams.length > 0) args.push(`${args.length === 0 ? "(" : ", "}headers?: ${operation.headerType}`);
|
|
1251
|
+
if (args.length === 0) return `(): Promise<${operation.responseType}>`;
|
|
1252
|
+
return `${args.join("")}): Promise<${operation.responseType}>`;
|
|
1253
|
+
}
|
|
1254
|
+
renderLiteral(value, indentLevel) {
|
|
1255
|
+
if (value === null) return "null";
|
|
1256
|
+
if (typeof value === "string") return `'${this.escapeStringLiteral(value)}'`;
|
|
1257
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
1258
|
+
if (Array.isArray(value)) {
|
|
1259
|
+
if (value.length === 0) return "[]";
|
|
1260
|
+
const nextIndent = this.indent(indentLevel + 1);
|
|
1261
|
+
const currentIndent = this.indent(indentLevel);
|
|
1262
|
+
return `[\n${value.map((entry) => `${nextIndent}${this.renderLiteral(entry, indentLevel + 1)}`).join(",\n")}\n${currentIndent}]`;
|
|
1064
1263
|
}
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
};
|
|
1074
|
-
const createRequestBody = (params, example, fallbackExample) => {
|
|
1075
|
-
const bodyParams = params.filter((param) => param.in === "body" || param.in === null);
|
|
1076
|
-
if (bodyParams.length === 0 && example == null) return;
|
|
1077
|
-
const schema = buildRequestBodySchema(bodyParams, example, fallbackExample);
|
|
1078
|
-
return {
|
|
1079
|
-
required: bodyParams.length > 0 ? bodyParams.some((param) => param.required) : false,
|
|
1080
|
-
content: { "application/json": {
|
|
1081
|
-
schema,
|
|
1082
|
-
...example != null ? { example } : {}
|
|
1083
|
-
} }
|
|
1084
|
-
};
|
|
1085
|
-
};
|
|
1086
|
-
const buildRequestBodySchema = (params, example, fallbackExample) => {
|
|
1087
|
-
const schema = mergeOpenApiSchemas(createExampleSchema(example), createExampleSchema(fallbackExample)) ?? { type: "object" };
|
|
1088
|
-
if (example != null) schema.example = example;
|
|
1089
|
-
else if (fallbackExample != null) schema.example = fallbackExample;
|
|
1090
|
-
for (const param of params) insertRequestBodyParam(schema, param);
|
|
1091
|
-
return schema;
|
|
1092
|
-
};
|
|
1093
|
-
const inferSchemaFromExample = (value) => {
|
|
1094
|
-
if (Array.isArray(value)) return {
|
|
1095
|
-
type: "array",
|
|
1096
|
-
items: inferSchemaFromExample(value[0]) ?? {},
|
|
1097
|
-
example: value
|
|
1098
|
-
};
|
|
1099
|
-
if (isRecord(value)) return {
|
|
1100
|
-
type: "object",
|
|
1101
|
-
properties: Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, inferSchemaFromExample(entryValue) ?? {}])),
|
|
1102
|
-
example: value
|
|
1103
|
-
};
|
|
1104
|
-
if (typeof value === "string") return {
|
|
1105
|
-
type: "string",
|
|
1106
|
-
example: value
|
|
1107
|
-
};
|
|
1108
|
-
if (typeof value === "number") return {
|
|
1109
|
-
type: Number.isInteger(value) ? "integer" : "number",
|
|
1110
|
-
example: value
|
|
1111
|
-
};
|
|
1112
|
-
if (typeof value === "boolean") return {
|
|
1113
|
-
type: "boolean",
|
|
1114
|
-
example: value
|
|
1115
|
-
};
|
|
1116
|
-
if (value === null) return {};
|
|
1117
|
-
};
|
|
1118
|
-
const insertRequestBodyParam = (rootSchema, param) => {
|
|
1119
|
-
const path = param.path.length > 0 ? param.path : [param.name];
|
|
1120
|
-
let currentSchema = rootSchema;
|
|
1121
|
-
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
1122
|
-
currentSchema.properties ??= {};
|
|
1123
|
-
currentSchema.properties[segment] ??= { type: "object" };
|
|
1124
|
-
if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], segment]));
|
|
1125
|
-
currentSchema = currentSchema.properties[segment];
|
|
1126
|
-
currentSchema.type ??= "object";
|
|
1127
|
-
if (index === path.length - 2 && param.required) currentSchema.required ??= [];
|
|
1128
|
-
}
|
|
1129
|
-
const leafKey = path[path.length - 1] ?? param.name;
|
|
1130
|
-
currentSchema.properties ??= {};
|
|
1131
|
-
currentSchema.properties[leafKey] = createParameterSchema(param);
|
|
1132
|
-
if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], leafKey]));
|
|
1133
|
-
};
|
|
1134
|
-
const createParameter = (param) => {
|
|
1135
|
-
return {
|
|
1136
|
-
name: param.name,
|
|
1137
|
-
in: param.in,
|
|
1138
|
-
required: param.in === "path" ? true : param.required,
|
|
1139
|
-
description: param.description ?? void 0,
|
|
1140
|
-
schema: createParameterSchema(param),
|
|
1141
|
-
example: param.defaultValue ?? void 0
|
|
1142
|
-
};
|
|
1143
|
-
};
|
|
1144
|
-
const createParameterSchema = (param) => {
|
|
1145
|
-
return {
|
|
1146
|
-
type: param.type ?? void 0,
|
|
1147
|
-
description: param.description ?? void 0,
|
|
1148
|
-
default: param.defaultValue ?? void 0
|
|
1149
|
-
};
|
|
1150
|
-
};
|
|
1151
|
-
const createResponses = (schemas, responseBodies) => {
|
|
1152
|
-
const responses = {};
|
|
1153
|
-
for (const schema of schemas) {
|
|
1154
|
-
if (!schema.statusCode) continue;
|
|
1155
|
-
const content = createResponseContent(responseBodies.filter((body) => body.statusCode === schema.statusCode));
|
|
1156
|
-
responses[schema.statusCode] = {
|
|
1157
|
-
description: schema.description ?? schema.statusCode,
|
|
1158
|
-
...content ? { content } : {}
|
|
1159
|
-
};
|
|
1264
|
+
if (typeof value === "object") {
|
|
1265
|
+
const entries = Object.entries(value);
|
|
1266
|
+
if (entries.length === 0) return "{}";
|
|
1267
|
+
const nextIndent = this.indent(indentLevel + 1);
|
|
1268
|
+
const currentIndent = this.indent(indentLevel);
|
|
1269
|
+
return `{\n${entries.map(([key, entry]) => `${nextIndent}${this.formatPropertyKey(key)}: ${this.renderLiteral(entry, indentLevel + 1)}`).join(",\n")}\n${currentIndent}}`;
|
|
1270
|
+
}
|
|
1271
|
+
return "undefined";
|
|
1160
1272
|
}
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1273
|
+
normalizeOpenApiDocument(document) {
|
|
1274
|
+
return this.normalizeObject(document, (key, value, parent) => {
|
|
1275
|
+
if (key === "example" && parent && typeof parent === "object" && !Array.isArray(parent)) {
|
|
1276
|
+
const owner = parent;
|
|
1277
|
+
if (owner.schema && this.isPlainObject(owner.schema)) return this.normalizeExample(value, owner.schema);
|
|
1278
|
+
if (this.isSchemaLike(owner)) return this.normalizeExample(value, owner);
|
|
1279
|
+
}
|
|
1280
|
+
return value;
|
|
1281
|
+
});
|
|
1168
1282
|
}
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
};
|
|
1283
|
+
normalizeObject(value, transform) {
|
|
1284
|
+
if (Array.isArray(value)) return value.map((entry) => this.normalizeObject(entry, transform)).filter((entry) => entry !== void 0);
|
|
1285
|
+
if (!this.isPlainObject(value)) return value;
|
|
1286
|
+
const output = {};
|
|
1287
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
1288
|
+
const transformed = transform(key, entry, value);
|
|
1289
|
+
const normalized = this.normalizeObject(transformed, transform);
|
|
1290
|
+
if (normalized !== void 0) output[key] = normalized;
|
|
1291
|
+
}
|
|
1292
|
+
return output;
|
|
1180
1293
|
}
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
const
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
const
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1294
|
+
normalizeExample(example, schema) {
|
|
1295
|
+
if (example === void 0) return;
|
|
1296
|
+
const type = typeof schema.type === "string" ? schema.type : void 0;
|
|
1297
|
+
if (type === "string") {
|
|
1298
|
+
if (typeof example === "string") return example;
|
|
1299
|
+
if (typeof example === "number" || typeof example === "boolean") return String(example);
|
|
1300
|
+
return;
|
|
1301
|
+
}
|
|
1302
|
+
if (type === "number" || type === "integer") {
|
|
1303
|
+
if (typeof example === "number") return example;
|
|
1304
|
+
if (typeof example === "string" && example.trim() !== "" && Number.isFinite(Number(example))) return Number(example);
|
|
1305
|
+
return;
|
|
1306
|
+
}
|
|
1307
|
+
if (type === "boolean") {
|
|
1308
|
+
if (typeof example === "boolean") return example;
|
|
1309
|
+
if (example === "true") return true;
|
|
1310
|
+
if (example === "false") return false;
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
if (type === "array") {
|
|
1314
|
+
if (!Array.isArray(example)) return;
|
|
1315
|
+
const itemSchema = this.isPlainObject(schema.items) ? schema.items : void 0;
|
|
1316
|
+
if (!itemSchema) return example;
|
|
1317
|
+
return example.map((entry) => this.normalizeExample(entry, itemSchema)).filter((entry) => entry !== void 0);
|
|
1318
|
+
}
|
|
1319
|
+
if (type === "object") {
|
|
1320
|
+
if (!this.isPlainObject(example)) return;
|
|
1321
|
+
const properties = this.isPlainObject(schema.properties) ? schema.properties : {};
|
|
1322
|
+
const required = Array.isArray(schema.required) ? schema.required.filter((entry) => typeof entry === "string") : [];
|
|
1323
|
+
const normalized = {};
|
|
1324
|
+
for (const [key, entry] of Object.entries(example)) {
|
|
1325
|
+
const propertySchema = properties[key];
|
|
1326
|
+
const normalizedEntry = propertySchema ? this.normalizeExample(entry, propertySchema) : entry;
|
|
1327
|
+
if (normalizedEntry !== void 0) normalized[key] = normalizedEntry;
|
|
1328
|
+
}
|
|
1329
|
+
for (const requiredKey of required) {
|
|
1330
|
+
if (requiredKey in normalized) continue;
|
|
1331
|
+
const propertySchema = properties[requiredKey];
|
|
1332
|
+
if (!propertySchema) return;
|
|
1333
|
+
const fallback = propertySchema.default !== void 0 ? this.normalizeExample(propertySchema.default, propertySchema) : propertySchema.example !== void 0 ? this.normalizeExample(propertySchema.example, propertySchema) : void 0;
|
|
1334
|
+
if (fallback === void 0) return;
|
|
1335
|
+
normalized[requiredKey] = fallback;
|
|
1336
|
+
}
|
|
1337
|
+
return normalized;
|
|
1338
|
+
}
|
|
1339
|
+
return example;
|
|
1340
|
+
}
|
|
1341
|
+
isPlainObject(value) {
|
|
1342
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1343
|
+
}
|
|
1344
|
+
isSchemaLike(value) {
|
|
1345
|
+
return "type" in value || "properties" in value || "items" in value || "required" in value || "default" in value;
|
|
1346
|
+
}
|
|
1347
|
+
indent(level) {
|
|
1348
|
+
return " ".repeat(level);
|
|
1349
|
+
}
|
|
1350
|
+
escapeStringLiteral(value) {
|
|
1351
|
+
return value.replace(/\\/g, String.raw`\\`).replace(/'/g, String.raw`\'`).replace(/\r/g, String.raw`\r`).replace(/\n/g, String.raw`\n`).replace(/\t/g, String.raw`\t`).replace(/\f/g, String.raw`\f`).replace(/\x08/g, String.raw`\b`).replace(/\u2028/g, String.raw`\u2028`).replace(/\u2029/g, String.raw`\u2029`);
|
|
1352
|
+
}
|
|
1353
|
+
isPathParam(segment) {
|
|
1354
|
+
return segment.startsWith("{") && segment.endsWith("}") || /^:[A-Za-z0-9_]+$/.test(segment);
|
|
1355
|
+
}
|
|
1356
|
+
stripPathParam(segment) {
|
|
1357
|
+
return segment.replace(/^\{/, "").replace(/\}$/, "").replace(/^:/, "");
|
|
1216
1358
|
}
|
|
1217
|
-
if (left.items || right.items) merged.items = mergeOpenApiSchemas(left.items ?? null, right.items ?? null) ?? {};
|
|
1218
|
-
if (left.required || right.required) merged.required = Array.from(new Set([...right.required ?? [], ...left.required ?? []]));
|
|
1219
|
-
return merged;
|
|
1220
|
-
};
|
|
1221
|
-
const buildOperationId = (method, path) => {
|
|
1222
|
-
return `${method}${path.replace(/\{([^}]+)\}/g, "$1").split("/").filter(Boolean).map((segment) => segment.replace(/[^a-zA-Z0-9]+/g, " ")).map((segment) => segment.trim()).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).replace(/\s+(.)/g, (_match, char) => char.toUpperCase())).join("")}`;
|
|
1223
1359
|
};
|
|
1224
|
-
|
|
1225
|
-
|
|
1360
|
+
|
|
1361
|
+
//#endregion
|
|
1362
|
+
//#region src/generator/TypeScriptNamingSupport.ts
|
|
1363
|
+
var TypeScriptNamingSupport = class TypeScriptNamingSupport {
|
|
1364
|
+
static contextualTailSegments = new Set([
|
|
1365
|
+
"history",
|
|
1366
|
+
"status",
|
|
1367
|
+
"detail",
|
|
1368
|
+
"details"
|
|
1369
|
+
]);
|
|
1370
|
+
static nestedContextSegments = new Set([
|
|
1371
|
+
"account",
|
|
1372
|
+
"accounts",
|
|
1373
|
+
"transaction",
|
|
1374
|
+
"transactions",
|
|
1375
|
+
"wallet",
|
|
1376
|
+
"wallets",
|
|
1377
|
+
"virtual-account",
|
|
1378
|
+
"virtual-accounts",
|
|
1379
|
+
"history"
|
|
1380
|
+
]);
|
|
1381
|
+
static roleSuffixes = [
|
|
1382
|
+
"Input",
|
|
1383
|
+
"Query",
|
|
1384
|
+
"Header",
|
|
1385
|
+
"Params"
|
|
1386
|
+
];
|
|
1387
|
+
sanitizeTypeName(value) {
|
|
1388
|
+
const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
1389
|
+
if (!normalized) return "GeneratedEntity";
|
|
1390
|
+
const pascalCased = normalized.split(/\s+/).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1)).join("");
|
|
1391
|
+
return /^[A-Za-z_$]/.test(pascalCased) ? pascalCased : `Type${pascalCased}`;
|
|
1392
|
+
}
|
|
1393
|
+
isPathParam(segment) {
|
|
1394
|
+
return segment.startsWith("{") && segment.endsWith("}") || /^:[A-Za-z0-9_]+$/.test(segment);
|
|
1395
|
+
}
|
|
1396
|
+
stripPathParam(segment) {
|
|
1397
|
+
return segment.replace(/^\{/, "").replace(/\}$/, "").replace(/^:/, "");
|
|
1398
|
+
}
|
|
1399
|
+
singularize(value) {
|
|
1400
|
+
if (/ies$/i.test(value)) return `${value.slice(0, -3)}y`;
|
|
1401
|
+
if (/(sses|shes|ches|xes|zes)$/i.test(value)) return value.slice(0, -2);
|
|
1402
|
+
if (value.endsWith("s") && !value.endsWith("ss") && value.length > 1) return value.slice(0, -1);
|
|
1403
|
+
return value;
|
|
1404
|
+
}
|
|
1405
|
+
pluralize(value) {
|
|
1406
|
+
if (/y$/i.test(value)) return `${value.slice(0, -1)}ies`;
|
|
1407
|
+
if (/s$/i.test(value)) return value;
|
|
1408
|
+
return `${value}s`;
|
|
1409
|
+
}
|
|
1410
|
+
toCamelCase(value) {
|
|
1411
|
+
const typeName = this.sanitizeTypeName(value);
|
|
1412
|
+
return typeName.charAt(0).toLowerCase() + typeName.slice(1);
|
|
1413
|
+
}
|
|
1414
|
+
deriveOperationNaming(path) {
|
|
1415
|
+
const pathSegments = this.getNormalizedPathSegments(path);
|
|
1416
|
+
const staticSegments = pathSegments.filter((segment) => !this.isPathParam(segment)).map((segment) => this.singularize(segment));
|
|
1417
|
+
const paramSegments = pathSegments.filter((segment) => this.isPathParam(segment)).map((segment) => this.singularize(this.stripPathParam(segment)));
|
|
1418
|
+
const tailSegment = staticSegments[staticSegments.length - 1] ?? "resource";
|
|
1419
|
+
const parentSegment = staticSegments[staticSegments.length - 2] ?? null;
|
|
1420
|
+
const hasPathParamBeforeTail = pathSegments.slice(0, -1).some((segment) => this.isPathParam(segment));
|
|
1421
|
+
const shouldPrefixParent = Boolean(parentSegment && (TypeScriptNamingSupport.contextualTailSegments.has(tailSegment.toLowerCase()) || hasPathParamBeforeTail && TypeScriptNamingSupport.nestedContextSegments.has(tailSegment.toLowerCase())));
|
|
1422
|
+
return {
|
|
1423
|
+
baseName: this.sanitizeTypeName(shouldPrefixParent ? `${parentSegment} ${tailSegment}` : tailSegment),
|
|
1424
|
+
collisionSuffix: paramSegments.length > 0 ? `By ${paramSegments.map((segment) => this.sanitizeTypeName(segment)).join(" And ")}` : parentSegment && !shouldPrefixParent ? this.sanitizeTypeName(parentSegment) : ""
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
fallbackCollisionSuffix(method, path, baseName) {
|
|
1428
|
+
const pathSegments = this.getNormalizedPathSegments(path);
|
|
1429
|
+
const staticSegments = pathSegments.filter((segment) => !this.isPathParam(segment));
|
|
1430
|
+
const tailSegment = staticSegments[staticSegments.length - 1] ?? "";
|
|
1431
|
+
const hasParams = pathSegments.some((segment) => this.isPathParam(segment));
|
|
1432
|
+
if (method === "get" && !hasParams && /s$/i.test(tailSegment)) return "List";
|
|
1433
|
+
if (method === "post" && !hasParams) return "Create";
|
|
1434
|
+
if ((method === "put" || method === "patch") && hasParams) return "Update";
|
|
1435
|
+
if (method === "delete") return "Delete";
|
|
1436
|
+
return `${this.sanitizeTypeName(method)}${baseName}`;
|
|
1437
|
+
}
|
|
1438
|
+
insertCollisionSuffix(baseName, collisionName) {
|
|
1439
|
+
if (!collisionName) return baseName;
|
|
1440
|
+
for (const roleSuffix of TypeScriptNamingSupport.roleSuffixes) if (baseName.endsWith(roleSuffix) && baseName.length > roleSuffix.length) return `${baseName.slice(0, -roleSuffix.length)}${collisionName}${roleSuffix}`;
|
|
1441
|
+
return `${baseName}${collisionName}`;
|
|
1442
|
+
}
|
|
1443
|
+
deriveSdkGroupNamesBySignature(document, namespaceStrategy) {
|
|
1444
|
+
const pathBySignature = /* @__PURE__ */ new Map();
|
|
1445
|
+
for (const path of Object.keys(document.paths)) {
|
|
1446
|
+
const signature = this.getStaticPathSignature(path);
|
|
1447
|
+
if (!pathBySignature.has(signature)) pathBySignature.set(signature, path);
|
|
1448
|
+
}
|
|
1449
|
+
const entries = Array.from(pathBySignature.entries()).map(([signature, path]) => ({
|
|
1450
|
+
signature,
|
|
1451
|
+
staticSegments: signature.split("/").filter(Boolean),
|
|
1452
|
+
candidates: this.buildSdkGroupNameCandidates(path, namespaceStrategy)
|
|
1453
|
+
})).sort((left, right) => {
|
|
1454
|
+
return left.staticSegments.length - right.staticSegments.length || left.signature.localeCompare(right.signature);
|
|
1455
|
+
});
|
|
1456
|
+
const groupNamesBySignature = /* @__PURE__ */ new Map();
|
|
1457
|
+
const usedNames = /* @__PURE__ */ new Set();
|
|
1458
|
+
for (const entry of entries) {
|
|
1459
|
+
const className = entry.candidates.find((candidate) => !usedNames.has(candidate)) ?? this.createUniqueSdkGroupName(entry.candidates[entry.candidates.length - 1] ?? "Resource", usedNames);
|
|
1460
|
+
usedNames.add(className);
|
|
1461
|
+
groupNamesBySignature.set(entry.signature, className);
|
|
1462
|
+
}
|
|
1463
|
+
return groupNamesBySignature;
|
|
1464
|
+
}
|
|
1465
|
+
getStaticPathSegments(path) {
|
|
1466
|
+
return this.getNormalizedPathSegments(path).filter((segment) => !this.isPathParam(segment)).map((segment) => this.singularize(segment));
|
|
1467
|
+
}
|
|
1468
|
+
getStaticPathSignature(path) {
|
|
1469
|
+
return this.getStaticPathSegments(path).join("/");
|
|
1470
|
+
}
|
|
1471
|
+
getNormalizedPathSegments(path) {
|
|
1472
|
+
return path.split("/").map((segment) => segment.trim()).filter(Boolean).filter((segment) => !/^v\d+$/i.test(segment));
|
|
1473
|
+
}
|
|
1474
|
+
deriveSdkMethodName(method, path, operation, methodStrategy) {
|
|
1475
|
+
if (methodStrategy === "operation-id" && operation.operationId) return this.toCamelCase(this.sanitizeTypeName(operation.operationId));
|
|
1476
|
+
const hasPathParams = this.getNormalizedPathSegments(path).some((segment) => this.isPathParam(segment));
|
|
1477
|
+
if (method === "get") return this.endsWithPluralStaticSegment(path) ? "list" : hasPathParams ? "get" : "list";
|
|
1478
|
+
if (method === "post") return "create";
|
|
1479
|
+
if (method === "patch" || method === "put") return "update";
|
|
1480
|
+
if (method === "delete") return "delete";
|
|
1481
|
+
return this.toCamelCase(this.sanitizeTypeName(method));
|
|
1482
|
+
}
|
|
1483
|
+
ensureUniqueSdkMethodNames(operations) {
|
|
1484
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1485
|
+
return operations.map((operation) => {
|
|
1486
|
+
const count = counts.get(operation.methodName) ?? 0;
|
|
1487
|
+
counts.set(operation.methodName, count + 1);
|
|
1488
|
+
if (count === 0) return operation;
|
|
1489
|
+
const suffix = this.sanitizeTypeName(this.fallbackCollisionSuffix(operation.method.toLowerCase(), operation.path, "Operation"));
|
|
1490
|
+
return {
|
|
1491
|
+
...operation,
|
|
1492
|
+
methodName: `${operation.methodName}${suffix}`
|
|
1493
|
+
};
|
|
1494
|
+
});
|
|
1495
|
+
}
|
|
1496
|
+
createSdkParameterManifest(parameters, location, path) {
|
|
1497
|
+
return [...(parameters ?? []).filter((parameter) => parameter.in === location).sort((left, right) => left.name.localeCompare(right.name)), ...this.getInferredPathParameters(path, location, (parameters ?? []).filter((parameter) => parameter.in === location))].sort((left, right) => left.name.localeCompare(right.name)).map((parameter) => ({
|
|
1498
|
+
name: parameter.name,
|
|
1499
|
+
accessor: this.toParameterAccessor(parameter.name),
|
|
1500
|
+
in: location,
|
|
1501
|
+
required: parameter.required ?? false,
|
|
1502
|
+
description: parameter.description
|
|
1503
|
+
}));
|
|
1504
|
+
}
|
|
1505
|
+
getInferredPathParameters(path, location, existingParameters) {
|
|
1506
|
+
if (location !== "path" || !path) return [];
|
|
1507
|
+
const existingNames = new Set(existingParameters.map((parameter) => parameter.name));
|
|
1508
|
+
return this.getNormalizedPathSegments(path).filter((segment) => this.isPathParam(segment)).map((segment) => this.stripPathParam(segment)).filter((name) => !existingNames.has(name)).map((name) => ({
|
|
1509
|
+
name,
|
|
1510
|
+
in: "path",
|
|
1511
|
+
required: true,
|
|
1512
|
+
schema: { type: "string" }
|
|
1513
|
+
}));
|
|
1514
|
+
}
|
|
1515
|
+
buildSdkGroupNameCandidates(path, namespaceStrategy) {
|
|
1516
|
+
const normalizedSegments = this.getNormalizedPathSegments(path);
|
|
1517
|
+
const rawStaticSegments = normalizedSegments.filter((segment) => !this.isPathParam(segment));
|
|
1518
|
+
const staticSegments = rawStaticSegments.map((segment) => this.singularize(segment));
|
|
1519
|
+
const defaultName = this.deriveOperationNaming(path).baseName;
|
|
1520
|
+
const preferredName = this.getPreferredSdkGroupName(normalizedSegments, rawStaticSegments, staticSegments);
|
|
1521
|
+
const contextualNames = staticSegments.map((_, index, segments) => this.sanitizeTypeName(segments.slice(index).join(" "))).reverse();
|
|
1522
|
+
if (namespaceStrategy === "scoped") return Array.from(new Set([
|
|
1523
|
+
preferredName ?? "",
|
|
1524
|
+
this.sanitizeTypeName(staticSegments.join(" ")),
|
|
1525
|
+
...contextualNames,
|
|
1526
|
+
defaultName
|
|
1527
|
+
].filter(Boolean)));
|
|
1528
|
+
return Array.from(new Set([
|
|
1529
|
+
preferredName ?? "",
|
|
1530
|
+
defaultName,
|
|
1531
|
+
...contextualNames
|
|
1532
|
+
].filter(Boolean)));
|
|
1533
|
+
}
|
|
1534
|
+
getPreferredSdkGroupName(normalizedSegments, rawStaticSegments, staticSegments) {
|
|
1535
|
+
const tailSegment = rawStaticSegments[rawStaticSegments.length - 1];
|
|
1536
|
+
const tailBaseSegment = staticSegments[staticSegments.length - 1];
|
|
1537
|
+
const parentSegment = staticSegments[staticSegments.length - 2];
|
|
1538
|
+
const hasPathParams = normalizedSegments.some((segment) => this.isPathParam(segment));
|
|
1539
|
+
const hasPathParamBeforeTail = normalizedSegments.slice(0, -1).some((segment) => this.isPathParam(segment));
|
|
1540
|
+
if (!tailSegment || !tailBaseSegment || !parentSegment) return null;
|
|
1541
|
+
if (rawStaticSegments.length === 2 && !hasPathParams) return this.sanitizeTypeName(`${tailBaseSegment} ${parentSegment}`);
|
|
1542
|
+
if (!hasPathParamBeforeTail) return null;
|
|
1543
|
+
if (this.singularize(tailSegment) !== tailSegment) return this.sanitizeTypeName(`${parentSegment} ${tailBaseSegment}`);
|
|
1544
|
+
return this.sanitizeTypeName(`${tailBaseSegment} ${parentSegment}`);
|
|
1545
|
+
}
|
|
1546
|
+
createUniqueSdkGroupName(baseName, usedNames) {
|
|
1547
|
+
let suffix = 2;
|
|
1548
|
+
let candidate = baseName;
|
|
1549
|
+
while (usedNames.has(candidate)) {
|
|
1550
|
+
candidate = `${baseName}${suffix}`;
|
|
1551
|
+
suffix += 1;
|
|
1552
|
+
}
|
|
1553
|
+
return candidate;
|
|
1554
|
+
}
|
|
1555
|
+
endsWithPluralStaticSegment(path) {
|
|
1556
|
+
const tailSegment = this.getNormalizedPathSegments(path).at(-1);
|
|
1557
|
+
if (!tailSegment || this.isPathParam(tailSegment)) return false;
|
|
1558
|
+
return this.singularize(tailSegment) !== tailSegment;
|
|
1559
|
+
}
|
|
1560
|
+
toParameterAccessor(value) {
|
|
1561
|
+
const normalized = value.replace(/[^A-Za-z0-9]+/g, " ").trim();
|
|
1562
|
+
if (!normalized) return "value";
|
|
1563
|
+
const [first, ...rest] = normalized.split(/\s+/).filter(Boolean);
|
|
1564
|
+
const camelValue = [first.toLowerCase(), ...rest.map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).toLowerCase())].join("");
|
|
1565
|
+
return /^[A-Za-z_$]/.test(camelValue) ? camelValue : `value${camelValue}`;
|
|
1566
|
+
}
|
|
1567
|
+
};
|
|
1568
|
+
|
|
1569
|
+
//#endregion
|
|
1570
|
+
//#region src/generator/TypeScriptShapeBuilder.ts
|
|
1571
|
+
var TypeScriptShapeBuilder = class {
|
|
1572
|
+
constructor(naming) {
|
|
1573
|
+
this.naming = naming;
|
|
1574
|
+
}
|
|
1575
|
+
createContext() {
|
|
1576
|
+
return {
|
|
1577
|
+
declarations: [],
|
|
1578
|
+
declarationByName: /* @__PURE__ */ new Map(),
|
|
1579
|
+
nameBySignature: /* @__PURE__ */ new Map(),
|
|
1580
|
+
usedNames: /* @__PURE__ */ new Set()
|
|
1581
|
+
};
|
|
1582
|
+
}
|
|
1583
|
+
namespaceTopLevelShape(shape, role) {
|
|
1584
|
+
if (shape.kind !== "object") return shape;
|
|
1585
|
+
return {
|
|
1586
|
+
...shape,
|
|
1587
|
+
signature: `${role}:${shape.signature}`
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
inferShapeFromExample(value, nameHint) {
|
|
1591
|
+
if (value === null) return {
|
|
1592
|
+
kind: "primitive",
|
|
1593
|
+
type: "null"
|
|
1594
|
+
};
|
|
1595
|
+
if (Array.isArray(value)) {
|
|
1596
|
+
if (value.length === 0) return {
|
|
1597
|
+
kind: "array",
|
|
1598
|
+
item: {
|
|
1599
|
+
kind: "primitive",
|
|
1600
|
+
type: "unknown"
|
|
1601
|
+
}
|
|
1602
|
+
};
|
|
1603
|
+
const itemShapes = this.dedupeShapes(value.map((entry) => this.inferShapeFromExample(entry, this.naming.singularize(nameHint))));
|
|
1604
|
+
return {
|
|
1605
|
+
kind: "array",
|
|
1606
|
+
item: itemShapes.length === 1 ? itemShapes[0] : {
|
|
1607
|
+
kind: "union",
|
|
1608
|
+
types: itemShapes
|
|
1609
|
+
}
|
|
1610
|
+
};
|
|
1611
|
+
}
|
|
1612
|
+
if (this.isRecord(value)) return this.createObjectShape(Object.entries(value).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
|
|
1613
|
+
key,
|
|
1614
|
+
optional: false,
|
|
1615
|
+
shape: this.inferShapeFromExample(entry, key)
|
|
1616
|
+
})));
|
|
1617
|
+
switch (typeof value) {
|
|
1618
|
+
case "string": return {
|
|
1619
|
+
kind: "primitive",
|
|
1620
|
+
type: "string"
|
|
1621
|
+
};
|
|
1622
|
+
case "number": return {
|
|
1623
|
+
kind: "primitive",
|
|
1624
|
+
type: "number"
|
|
1625
|
+
};
|
|
1626
|
+
case "boolean": return {
|
|
1627
|
+
kind: "primitive",
|
|
1628
|
+
type: "boolean"
|
|
1629
|
+
};
|
|
1630
|
+
default: return {
|
|
1631
|
+
kind: "primitive",
|
|
1632
|
+
type: "unknown"
|
|
1633
|
+
};
|
|
1634
|
+
}
|
|
1635
|
+
}
|
|
1636
|
+
registerNamedShape(shape, preferredName, context, collisionSuffix) {
|
|
1637
|
+
if (shape.kind === "object") return this.registerObjectShape(shape, preferredName, context, collisionSuffix, true);
|
|
1638
|
+
const name = this.createUniqueTypeName(preferredName, context, collisionSuffix);
|
|
1639
|
+
const declaration = {
|
|
1640
|
+
kind: "shape-alias",
|
|
1641
|
+
name,
|
|
1642
|
+
shape: this.prepareNestedShape(shape, preferredName, context)
|
|
1643
|
+
};
|
|
1644
|
+
context.declarations.push(declaration);
|
|
1645
|
+
context.declarationByName.set(name, declaration);
|
|
1646
|
+
return name;
|
|
1647
|
+
}
|
|
1648
|
+
registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias = false) {
|
|
1649
|
+
const existingName = context.nameBySignature.get(shape.signature);
|
|
1650
|
+
const compatibleDeclaration = this.findCompatibleObjectDeclaration(shape, preferredName, context);
|
|
1651
|
+
if (existingName) {
|
|
1652
|
+
if (emitAlias && existingName !== preferredName && !context.declarationByName.has(preferredName)) {
|
|
1653
|
+
const aliasName = this.createUniqueTypeName(preferredName, context, collisionSuffix);
|
|
1654
|
+
if (aliasName !== existingName) {
|
|
1655
|
+
const aliasDeclaration = {
|
|
1656
|
+
kind: "interface-alias",
|
|
1657
|
+
name: aliasName,
|
|
1658
|
+
target: existingName
|
|
1659
|
+
};
|
|
1660
|
+
context.declarations.push(aliasDeclaration);
|
|
1661
|
+
context.declarationByName.set(aliasName, aliasDeclaration);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
return existingName;
|
|
1665
|
+
}
|
|
1666
|
+
if (compatibleDeclaration) {
|
|
1667
|
+
if (this.isObjectShapeAssignableTo(shape, compatibleDeclaration.rawShape)) {
|
|
1668
|
+
context.nameBySignature.set(shape.signature, compatibleDeclaration.name);
|
|
1669
|
+
return compatibleDeclaration.name;
|
|
1670
|
+
}
|
|
1671
|
+
const mergedShape = this.mergeObjectShapes(compatibleDeclaration.rawShape, shape);
|
|
1672
|
+
compatibleDeclaration.rawShape = mergedShape;
|
|
1673
|
+
compatibleDeclaration.properties = mergedShape.properties.map((property) => ({
|
|
1674
|
+
...property,
|
|
1675
|
+
shape: this.prepareNestedShape(property.shape, property.key, context)
|
|
1676
|
+
}));
|
|
1677
|
+
context.nameBySignature.set(shape.signature, compatibleDeclaration.name);
|
|
1678
|
+
context.nameBySignature.set(mergedShape.signature, compatibleDeclaration.name);
|
|
1679
|
+
return compatibleDeclaration.name;
|
|
1680
|
+
}
|
|
1681
|
+
const declarationName = this.createUniqueTypeName(preferredName, context, collisionSuffix);
|
|
1682
|
+
const declaration = {
|
|
1683
|
+
kind: "interface",
|
|
1684
|
+
name: declarationName,
|
|
1685
|
+
baseName: this.naming.sanitizeTypeName(preferredName),
|
|
1686
|
+
rawShape: shape,
|
|
1687
|
+
properties: []
|
|
1688
|
+
};
|
|
1689
|
+
context.nameBySignature.set(shape.signature, declarationName);
|
|
1690
|
+
context.declarations.push(declaration);
|
|
1691
|
+
context.declarationByName.set(declarationName, declaration);
|
|
1692
|
+
declaration.properties = shape.properties.map((property) => ({
|
|
1693
|
+
...property,
|
|
1694
|
+
shape: this.prepareNestedShape(property.shape, property.key, context)
|
|
1695
|
+
}));
|
|
1696
|
+
return declarationName;
|
|
1697
|
+
}
|
|
1698
|
+
resolveSdkResponseType(responses, fallbackType) {
|
|
1699
|
+
const successResponse = Object.entries(responses).filter(([statusCode]) => /^2\d\d$/.test(statusCode)).sort(([left], [right]) => left.localeCompare(right))[0]?.[1];
|
|
1700
|
+
if (!successResponse) return fallbackType;
|
|
1701
|
+
const mediaType = this.getPreferredMediaType(successResponse.content);
|
|
1702
|
+
if (!mediaType) return fallbackType;
|
|
1703
|
+
const responseSchema = this.resolveResponsePayloadSchema(mediaType.schema, mediaType.example).schema ?? mediaType.schema;
|
|
1704
|
+
return responseSchema && this.resolveSchemaType(responseSchema) === "array" ? `${fallbackType}[]` : fallbackType;
|
|
1705
|
+
}
|
|
1706
|
+
getSuccessResponseShape(responses) {
|
|
1707
|
+
const successResponse = Object.entries(responses).filter(([statusCode]) => /^2\d\d$/.test(statusCode)).sort(([left], [right]) => left.localeCompare(right))[0]?.[1];
|
|
1708
|
+
if (!successResponse) return this.emptyObjectShape;
|
|
1709
|
+
const mediaType = this.getPreferredMediaType(successResponse.content);
|
|
1710
|
+
if (!mediaType) return this.emptyObjectShape;
|
|
1711
|
+
const payload = this.resolveResponsePayloadSchema(mediaType.schema, mediaType.example);
|
|
1712
|
+
if (!payload.schema) return this.schemaToShape(mediaType.schema, "Response", mediaType.example);
|
|
1713
|
+
if (this.resolveSchemaType(payload.schema) === "array") return this.schemaToShape(payload.schema.items, "Item", this.extractExampleArrayItem(payload.example));
|
|
1714
|
+
return this.schemaToShape(payload.schema, "Response", payload.example);
|
|
1715
|
+
}
|
|
1716
|
+
getRequestInputShape(requestBody) {
|
|
1717
|
+
if (!requestBody) return this.emptyObjectShape;
|
|
1718
|
+
const mediaType = this.getPreferredMediaType(requestBody.content);
|
|
1719
|
+
if (!mediaType) return this.emptyObjectShape;
|
|
1720
|
+
return this.schemaToShape(mediaType.schema, "Input", mediaType.example);
|
|
1721
|
+
}
|
|
1722
|
+
getResponseExampleShape(responses) {
|
|
1723
|
+
const shapes = Object.entries(responses).sort(([left], [right]) => left.localeCompare(right)).flatMap(([, response]) => {
|
|
1724
|
+
const mediaType = this.getPreferredMediaType(response.content);
|
|
1725
|
+
if (!mediaType) return [];
|
|
1726
|
+
const fullExample = mediaType.example ?? mediaType.schema?.example;
|
|
1727
|
+
if (mediaType.schema) return [this.schemaToShape(mediaType.schema, "ResponseExample", fullExample)];
|
|
1728
|
+
if (fullExample !== void 0) return [this.inferShapeFromExample(fullExample, "ResponseExample")];
|
|
1729
|
+
return [];
|
|
1730
|
+
});
|
|
1731
|
+
const uniqueShapes = this.dedupeShapes(shapes);
|
|
1732
|
+
if (uniqueShapes.length === 0) return {
|
|
1733
|
+
kind: "primitive",
|
|
1734
|
+
type: "unknown"
|
|
1735
|
+
};
|
|
1736
|
+
return uniqueShapes.length === 1 ? uniqueShapes[0] : {
|
|
1737
|
+
kind: "union",
|
|
1738
|
+
types: uniqueShapes
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
createParameterGroupShape(parameters, location, path) {
|
|
1742
|
+
const relevantParameters = (parameters ?? []).filter((parameter) => parameter.in === location).sort((left, right) => left.name.localeCompare(right.name));
|
|
1743
|
+
const mergedParameters = [...relevantParameters, ...this.naming.getInferredPathParameters(path, location, relevantParameters)].sort((left, right) => left.name.localeCompare(right.name));
|
|
1744
|
+
if (mergedParameters.length === 0) return this.emptyObjectShape;
|
|
1745
|
+
return this.createObjectShape(mergedParameters.map((parameter) => ({
|
|
1746
|
+
key: parameter.name,
|
|
1747
|
+
optional: !(parameter.required ?? false),
|
|
1748
|
+
shape: this.schemaToShape(parameter.schema, parameter.name, parameter.example)
|
|
1749
|
+
})));
|
|
1750
|
+
}
|
|
1751
|
+
get emptyObjectShape() {
|
|
1752
|
+
return this.createObjectShape([]);
|
|
1753
|
+
}
|
|
1754
|
+
findCompatibleObjectDeclaration(shape, preferredName, context) {
|
|
1755
|
+
const baseName = this.naming.sanitizeTypeName(preferredName);
|
|
1756
|
+
return context.declarations.find((declaration) => {
|
|
1757
|
+
if (declaration.kind !== "interface" || declaration.baseName !== baseName) return false;
|
|
1758
|
+
return this.isObjectShapeAssignableTo(shape, declaration.rawShape) || this.isObjectShapeAssignableTo(declaration.rawShape, shape) || this.canMergeObjectShapes(declaration.rawShape, shape);
|
|
1759
|
+
});
|
|
1760
|
+
}
|
|
1761
|
+
canMergeObjectShapes(left, right) {
|
|
1762
|
+
const keys = new Set([...left.properties.map((property) => property.key), ...right.properties.map((property) => property.key)]);
|
|
1763
|
+
for (const key of keys) {
|
|
1764
|
+
const leftProperty = left.properties.find((property) => property.key === key);
|
|
1765
|
+
const rightProperty = right.properties.find((property) => property.key === key);
|
|
1766
|
+
if (!leftProperty || !rightProperty) {
|
|
1767
|
+
if (!(leftProperty ?? rightProperty)?.optional) return false;
|
|
1768
|
+
continue;
|
|
1769
|
+
}
|
|
1770
|
+
if (!this.canMergeShapes(leftProperty.shape, rightProperty.shape)) return false;
|
|
1771
|
+
}
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
isObjectShapeAssignableTo(source, target) {
|
|
1775
|
+
const targetProperties = new Map(target.properties.map((property) => [property.key, property]));
|
|
1776
|
+
for (const sourceProperty of source.properties) {
|
|
1777
|
+
const targetProperty = targetProperties.get(sourceProperty.key);
|
|
1778
|
+
if (!targetProperty) return false;
|
|
1779
|
+
if (sourceProperty.optional && !targetProperty.optional) return false;
|
|
1780
|
+
if (!this.isShapeAssignableTo(sourceProperty.shape, targetProperty.shape)) return false;
|
|
1781
|
+
}
|
|
1782
|
+
return target.properties.every((targetProperty) => {
|
|
1783
|
+
return source.properties.some((sourceProperty) => sourceProperty.key === targetProperty.key) || targetProperty.optional;
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
isShapeAssignableTo(source, target) {
|
|
1787
|
+
if (target.kind === "union") return target.types.some((targetType) => this.isShapeAssignableTo(source, targetType));
|
|
1788
|
+
switch (source.kind) {
|
|
1789
|
+
case "primitive":
|
|
1790
|
+
if (target.kind !== "primitive") return false;
|
|
1791
|
+
return source.type === target.type;
|
|
1792
|
+
case "array":
|
|
1793
|
+
if (target.kind !== "array") return false;
|
|
1794
|
+
return this.isShapeAssignableTo(source.item, target.item);
|
|
1795
|
+
case "union": return source.types.every((sourceType) => this.isShapeAssignableTo(sourceType, target));
|
|
1796
|
+
case "object":
|
|
1797
|
+
if (target.kind !== "object") return false;
|
|
1798
|
+
return this.isObjectShapeAssignableTo(source, target);
|
|
1799
|
+
}
|
|
1800
|
+
}
|
|
1801
|
+
canMergeShapes(left, right) {
|
|
1802
|
+
if (left.kind === "union") return left.types.every((leftType) => this.canMergeShapes(leftType, right));
|
|
1803
|
+
if (right.kind === "union") return right.types.every((rightType) => this.canMergeShapes(left, rightType));
|
|
1804
|
+
if (left.kind === "primitive" && right.kind === "primitive") return true;
|
|
1805
|
+
if (left.kind === "array" && right.kind === "array") return this.canMergeShapes(left.item, right.item);
|
|
1806
|
+
if (left.kind === "object" && right.kind === "object") return this.canMergeObjectShapes(left, right);
|
|
1807
|
+
return false;
|
|
1808
|
+
}
|
|
1809
|
+
mergeObjectShapes(left, right) {
|
|
1810
|
+
const keys = new Set([...left.properties.map((property) => property.key), ...right.properties.map((property) => property.key)]);
|
|
1811
|
+
return this.createObjectShape(Array.from(keys).map((key) => {
|
|
1812
|
+
const leftProperty = left.properties.find((property) => property.key === key);
|
|
1813
|
+
const rightProperty = right.properties.find((property) => property.key === key);
|
|
1814
|
+
if (leftProperty && rightProperty) return {
|
|
1815
|
+
key,
|
|
1816
|
+
optional: leftProperty.optional || rightProperty.optional,
|
|
1817
|
+
shape: this.mergeShapes(leftProperty.shape, rightProperty.shape)
|
|
1818
|
+
};
|
|
1819
|
+
return {
|
|
1820
|
+
key,
|
|
1821
|
+
optional: true,
|
|
1822
|
+
shape: (leftProperty ?? rightProperty).shape
|
|
1823
|
+
};
|
|
1824
|
+
}));
|
|
1825
|
+
}
|
|
1826
|
+
mergeShapes(left, right) {
|
|
1827
|
+
if (left.kind === "union" || right.kind === "union") return this.createUnionShape(left, right);
|
|
1828
|
+
if (left.kind !== right.kind) return this.createUnionShape(left, right);
|
|
1829
|
+
switch (left.kind) {
|
|
1830
|
+
case "primitive": return right.kind === "primitive" && left.type === right.type ? left : this.createUnionShape(left, right);
|
|
1831
|
+
case "array":
|
|
1832
|
+
if (right.kind !== "array") return left;
|
|
1833
|
+
return {
|
|
1834
|
+
kind: "array",
|
|
1835
|
+
item: this.mergeShapes(left.item, right.item)
|
|
1836
|
+
};
|
|
1837
|
+
case "object":
|
|
1838
|
+
if (right.kind !== "object") return left;
|
|
1839
|
+
return this.mergeObjectShapes(left, right);
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
createUnionShape(...shapes) {
|
|
1843
|
+
const flattened = shapes.flatMap((shape) => shape.kind === "union" ? shape.types : [shape]);
|
|
1844
|
+
const deduped = this.dedupeShapes(flattened);
|
|
1845
|
+
return deduped.length === 1 ? deduped[0] : {
|
|
1846
|
+
kind: "union",
|
|
1847
|
+
types: deduped
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
prepareNestedShape(shape, keyHint, context) {
|
|
1851
|
+
if (shape.kind === "object") return {
|
|
1852
|
+
kind: "primitive",
|
|
1853
|
+
type: this.registerObjectShape(shape, this.naming.sanitizeTypeName(this.naming.singularize(keyHint)), context, this.naming.sanitizeTypeName(keyHint))
|
|
1854
|
+
};
|
|
1855
|
+
if (shape.kind === "array") return {
|
|
1856
|
+
kind: "array",
|
|
1857
|
+
item: this.prepareNestedShape(shape.item, this.naming.singularize(keyHint), context)
|
|
1858
|
+
};
|
|
1859
|
+
if (shape.kind === "union") {
|
|
1860
|
+
const preparedTypes = this.dedupeShapes(shape.types.map((entry, index) => {
|
|
1861
|
+
return this.prepareNestedShape(entry, this.getUnionMemberKeyHint(keyHint, index, entry), context);
|
|
1862
|
+
}));
|
|
1863
|
+
if (preparedTypes.length === 1) return preparedTypes[0];
|
|
1864
|
+
return {
|
|
1865
|
+
kind: "union",
|
|
1866
|
+
types: preparedTypes
|
|
1867
|
+
};
|
|
1868
|
+
}
|
|
1869
|
+
return shape;
|
|
1870
|
+
}
|
|
1871
|
+
getUnionMemberKeyHint(keyHint, index, shape) {
|
|
1872
|
+
if (shape.kind !== "object" && shape.kind !== "array") return keyHint;
|
|
1873
|
+
const sanitizedKeyHint = this.naming.sanitizeTypeName(keyHint);
|
|
1874
|
+
if (sanitizedKeyHint.endsWith("ResponseExample")) return `${sanitizedKeyHint}Variant${index + 1}`;
|
|
1875
|
+
return keyHint;
|
|
1876
|
+
}
|
|
1877
|
+
schemaToShape(schema, nameHint, fallbackExample) {
|
|
1878
|
+
if (!schema) return this.inferShapeFromExample(fallbackExample, nameHint);
|
|
1879
|
+
const schemaType = this.resolveSchemaType(schema);
|
|
1880
|
+
if (schemaType === "array") return {
|
|
1881
|
+
kind: "array",
|
|
1882
|
+
item: this.schemaToShape(schema.items, this.naming.singularize(nameHint), this.extractExampleArrayItem(schema.example) ?? this.extractExampleArrayItem(fallbackExample))
|
|
1883
|
+
};
|
|
1884
|
+
if (schemaType === "object") {
|
|
1885
|
+
const propertyExamples = this.isRecord(schema.example) ? schema.example : this.isRecord(fallbackExample) ? fallbackExample : void 0;
|
|
1886
|
+
const properties = Object.entries(schema.properties ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([key, entry]) => ({
|
|
1887
|
+
key,
|
|
1888
|
+
optional: !(schema.required ?? []).includes(key),
|
|
1889
|
+
shape: this.schemaToShape(entry, key, propertyExamples?.[key])
|
|
1890
|
+
}));
|
|
1891
|
+
if (properties.length > 0) return this.createObjectShape(properties);
|
|
1892
|
+
return this.inferShapeFromExample(schema.example ?? fallbackExample, nameHint);
|
|
1893
|
+
}
|
|
1894
|
+
if (schemaType === "integer" || schemaType === "number") return {
|
|
1895
|
+
kind: "primitive",
|
|
1896
|
+
type: "number"
|
|
1897
|
+
};
|
|
1898
|
+
if (schemaType === "string") return {
|
|
1899
|
+
kind: "primitive",
|
|
1900
|
+
type: "string"
|
|
1901
|
+
};
|
|
1902
|
+
if (schemaType === "boolean") return {
|
|
1903
|
+
kind: "primitive",
|
|
1904
|
+
type: "boolean"
|
|
1905
|
+
};
|
|
1906
|
+
if (schema.example === null || fallbackExample === null) return {
|
|
1907
|
+
kind: "primitive",
|
|
1908
|
+
type: "null"
|
|
1909
|
+
};
|
|
1910
|
+
if (schema.example !== void 0 || fallbackExample !== void 0) return this.inferShapeFromExample(schema.example ?? fallbackExample, nameHint);
|
|
1911
|
+
return {
|
|
1912
|
+
kind: "primitive",
|
|
1913
|
+
type: "unknown"
|
|
1914
|
+
};
|
|
1915
|
+
}
|
|
1916
|
+
dedupeShapes(shapes) {
|
|
1917
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1918
|
+
return shapes.filter((shape) => {
|
|
1919
|
+
const signature = this.getShapeSignature(shape);
|
|
1920
|
+
if (seen.has(signature)) return false;
|
|
1921
|
+
seen.add(signature);
|
|
1922
|
+
return true;
|
|
1923
|
+
});
|
|
1924
|
+
}
|
|
1925
|
+
createObjectShape(properties) {
|
|
1926
|
+
const normalizedProperties = properties.map((property) => ({ ...property })).sort((left, right) => left.key.localeCompare(right.key));
|
|
1927
|
+
return {
|
|
1928
|
+
kind: "object",
|
|
1929
|
+
signature: JSON.stringify(normalizedProperties.map((property) => ({
|
|
1930
|
+
key: property.key,
|
|
1931
|
+
optional: property.optional,
|
|
1932
|
+
shape: this.getShapeSignature(property.shape)
|
|
1933
|
+
}))),
|
|
1934
|
+
properties: normalizedProperties
|
|
1935
|
+
};
|
|
1936
|
+
}
|
|
1937
|
+
getShapeSignature(shape) {
|
|
1938
|
+
switch (shape.kind) {
|
|
1939
|
+
case "primitive": return `primitive:${shape.type}`;
|
|
1940
|
+
case "array": return `array:${this.getShapeSignature(shape.item)}`;
|
|
1941
|
+
case "union": return `union:${shape.types.map((entry) => this.getShapeSignature(entry)).join("|")}`;
|
|
1942
|
+
case "object": return `object:${shape.signature}`;
|
|
1943
|
+
}
|
|
1944
|
+
}
|
|
1945
|
+
getPreferredMediaType(content) {
|
|
1946
|
+
if (!content) return;
|
|
1947
|
+
return content["application/json"] ?? content["application/*+json"] ?? Object.values(content)[0];
|
|
1948
|
+
}
|
|
1949
|
+
resolveResponsePayloadSchema(schema, example) {
|
|
1950
|
+
for (const path of [["data"], ["meta", "data"]]) {
|
|
1951
|
+
const candidate = this.getSchemaCandidateAtPath(schema, example, path);
|
|
1952
|
+
if (candidate) return candidate;
|
|
1953
|
+
}
|
|
1954
|
+
return {};
|
|
1955
|
+
}
|
|
1956
|
+
getSchemaCandidateAtPath(schema, example, path) {
|
|
1957
|
+
const schemaAtPath = this.getSchemaAtPath(schema, path);
|
|
1958
|
+
const exampleAtPath = this.getExampleAtPath(example, path);
|
|
1959
|
+
if (!schemaAtPath && exampleAtPath === void 0) return;
|
|
1960
|
+
if (schemaAtPath) return {
|
|
1961
|
+
schema: schemaAtPath.example === void 0 && exampleAtPath !== void 0 ? {
|
|
1962
|
+
...schemaAtPath,
|
|
1963
|
+
example: exampleAtPath
|
|
1964
|
+
} : schemaAtPath,
|
|
1965
|
+
example: exampleAtPath ?? schemaAtPath.example
|
|
1966
|
+
};
|
|
1967
|
+
return {
|
|
1968
|
+
schema: {
|
|
1969
|
+
...this.inferSchemaTypeFromExample(exampleAtPath),
|
|
1970
|
+
example: exampleAtPath
|
|
1971
|
+
},
|
|
1972
|
+
example: exampleAtPath
|
|
1973
|
+
};
|
|
1974
|
+
}
|
|
1975
|
+
getSchemaAtPath(schema, path) {
|
|
1976
|
+
let currentSchema = schema;
|
|
1977
|
+
for (const segment of path) {
|
|
1978
|
+
if (!currentSchema?.properties?.[segment]) return;
|
|
1979
|
+
currentSchema = currentSchema.properties[segment];
|
|
1980
|
+
}
|
|
1981
|
+
return currentSchema;
|
|
1982
|
+
}
|
|
1983
|
+
getExampleAtPath(example, path) {
|
|
1984
|
+
let currentValue = example;
|
|
1985
|
+
for (const segment of path) {
|
|
1986
|
+
if (!this.isRecord(currentValue) || !(segment in currentValue)) return;
|
|
1987
|
+
currentValue = currentValue[segment];
|
|
1988
|
+
}
|
|
1989
|
+
return currentValue;
|
|
1990
|
+
}
|
|
1991
|
+
inferSchemaTypeFromExample(value) {
|
|
1992
|
+
if (Array.isArray(value)) return {
|
|
1993
|
+
type: "array",
|
|
1994
|
+
items: value.map((entry) => this.inferSchemaTypeFromExample(entry)).find((entry) => this.hasSchemaDetails(entry)) ?? {}
|
|
1995
|
+
};
|
|
1996
|
+
if (this.isRecord(value)) return {
|
|
1997
|
+
type: "object",
|
|
1998
|
+
properties: Object.fromEntries(Object.entries(value).map(([key, entry]) => [key, this.inferSchemaTypeFromExample(entry)]))
|
|
1999
|
+
};
|
|
2000
|
+
if (typeof value === "string") return { type: "string" };
|
|
2001
|
+
if (typeof value === "number") return { type: "number" };
|
|
2002
|
+
if (typeof value === "boolean") return { type: "boolean" };
|
|
2003
|
+
return {};
|
|
2004
|
+
}
|
|
2005
|
+
hasSchemaDetails(schema) {
|
|
2006
|
+
return Boolean(schema?.type || schema?.properties || schema?.items || schema?.example !== void 0);
|
|
2007
|
+
}
|
|
2008
|
+
resolveSchemaType(schema) {
|
|
2009
|
+
return schema.type ?? (schema.properties ? "object" : void 0);
|
|
2010
|
+
}
|
|
2011
|
+
extractExampleArrayItem(value) {
|
|
2012
|
+
return Array.isArray(value) ? value[0] : void 0;
|
|
2013
|
+
}
|
|
2014
|
+
createUniqueTypeName(preferredName, context, collisionSuffix) {
|
|
2015
|
+
const baseName = this.naming.sanitizeTypeName(preferredName) || "GeneratedEntity";
|
|
2016
|
+
const collisionName = this.naming.sanitizeTypeName(collisionSuffix);
|
|
2017
|
+
let candidate = baseName;
|
|
2018
|
+
let suffix = 2;
|
|
2019
|
+
if (!context.usedNames.has(candidate)) {
|
|
2020
|
+
context.usedNames.add(candidate);
|
|
2021
|
+
return candidate;
|
|
2022
|
+
}
|
|
2023
|
+
candidate = this.naming.insertCollisionSuffix(baseName, collisionName);
|
|
2024
|
+
if (!context.usedNames.has(candidate)) {
|
|
2025
|
+
context.usedNames.add(candidate);
|
|
2026
|
+
return candidate;
|
|
2027
|
+
}
|
|
2028
|
+
while (context.usedNames.has(candidate)) {
|
|
2029
|
+
candidate = `${baseName}${suffix}`;
|
|
2030
|
+
suffix += 1;
|
|
2031
|
+
}
|
|
2032
|
+
context.usedNames.add(candidate);
|
|
2033
|
+
return candidate;
|
|
2034
|
+
}
|
|
2035
|
+
isRecord(value) {
|
|
2036
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2037
|
+
}
|
|
2038
|
+
};
|
|
2039
|
+
|
|
2040
|
+
//#endregion
|
|
2041
|
+
//#region src/generator/TypeScriptTypeBuilder.ts
|
|
2042
|
+
var TypeScriptTypeBuilder = class {
|
|
2043
|
+
naming = new TypeScriptNamingSupport();
|
|
2044
|
+
shapes = new TypeScriptShapeBuilder(this.naming);
|
|
2045
|
+
createContext() {
|
|
2046
|
+
return this.shapes.createContext();
|
|
2047
|
+
}
|
|
2048
|
+
collectSemanticModels(document) {
|
|
2049
|
+
const models = [];
|
|
2050
|
+
for (const [path, operations] of Object.entries(document.paths)) {
|
|
2051
|
+
const naming = this.naming.deriveOperationNaming(path);
|
|
2052
|
+
const baseName = naming.baseName;
|
|
2053
|
+
const sortedOperations = Object.entries(operations).sort(([, leftOperation], [, rightOperation]) => {
|
|
2054
|
+
return this.getOperationPriority(rightOperation) - this.getOperationPriority(leftOperation);
|
|
2055
|
+
});
|
|
2056
|
+
for (const [method, operation] of sortedOperations) {
|
|
2057
|
+
const collisionSuffix = naming.collisionSuffix || this.naming.fallbackCollisionSuffix(method, path, baseName);
|
|
2058
|
+
models.push({
|
|
2059
|
+
path,
|
|
2060
|
+
method,
|
|
2061
|
+
name: baseName,
|
|
2062
|
+
role: "response",
|
|
2063
|
+
shape: this.shapes.getSuccessResponseShape(operation.responses),
|
|
2064
|
+
collisionSuffix
|
|
2065
|
+
});
|
|
2066
|
+
models.push({
|
|
2067
|
+
path,
|
|
2068
|
+
method,
|
|
2069
|
+
name: `${baseName}ResponseExample`,
|
|
2070
|
+
role: "responseExample",
|
|
2071
|
+
shape: this.shapes.getResponseExampleShape(operation.responses),
|
|
2072
|
+
collisionSuffix
|
|
2073
|
+
});
|
|
2074
|
+
models.push({
|
|
2075
|
+
path,
|
|
2076
|
+
method,
|
|
2077
|
+
name: `${baseName}Input`,
|
|
2078
|
+
role: "input",
|
|
2079
|
+
shape: this.shapes.getRequestInputShape(operation.requestBody),
|
|
2080
|
+
collisionSuffix
|
|
2081
|
+
});
|
|
2082
|
+
models.push({
|
|
2083
|
+
path,
|
|
2084
|
+
method,
|
|
2085
|
+
name: `${baseName}Query`,
|
|
2086
|
+
role: "query",
|
|
2087
|
+
shape: this.shapes.createParameterGroupShape(operation.parameters, "query", path),
|
|
2088
|
+
collisionSuffix
|
|
2089
|
+
});
|
|
2090
|
+
models.push({
|
|
2091
|
+
path,
|
|
2092
|
+
method,
|
|
2093
|
+
name: `${baseName}Header`,
|
|
2094
|
+
role: "header",
|
|
2095
|
+
shape: this.shapes.createParameterGroupShape(operation.parameters, "header", path),
|
|
2096
|
+
collisionSuffix
|
|
2097
|
+
});
|
|
2098
|
+
models.push({
|
|
2099
|
+
path,
|
|
2100
|
+
method,
|
|
2101
|
+
name: `${baseName}Params`,
|
|
2102
|
+
role: "params",
|
|
2103
|
+
shape: this.shapes.createParameterGroupShape(operation.parameters, "path", path),
|
|
2104
|
+
collisionSuffix
|
|
2105
|
+
});
|
|
2106
|
+
}
|
|
2107
|
+
}
|
|
2108
|
+
return models;
|
|
2109
|
+
}
|
|
2110
|
+
buildSdkManifest(document, operationTypeRefs, options = {}) {
|
|
2111
|
+
const sdkGroupNamesBySignature = this.naming.deriveSdkGroupNamesBySignature(document, options.namespaceStrategy ?? "smart");
|
|
2112
|
+
const groups = /* @__PURE__ */ new Map();
|
|
2113
|
+
for (const [path, operations] of Object.entries(document.paths)) {
|
|
2114
|
+
const staticSignature = this.naming.getStaticPathSegments(path).join("/");
|
|
2115
|
+
const className = sdkGroupNamesBySignature.get(staticSignature) ?? "Resource";
|
|
2116
|
+
const propertyName = this.naming.toCamelCase(this.naming.pluralize(className));
|
|
2117
|
+
const group = groups.get(propertyName) ?? {
|
|
2118
|
+
className,
|
|
2119
|
+
propertyName,
|
|
2120
|
+
operations: []
|
|
2121
|
+
};
|
|
2122
|
+
for (const [method, operation] of Object.entries(operations)) {
|
|
2123
|
+
const refs = operationTypeRefs.get(`${path}::${method}`) ?? {
|
|
2124
|
+
response: "Record<string, never>",
|
|
2125
|
+
responseExample: "unknown",
|
|
2126
|
+
input: "Record<string, never>",
|
|
2127
|
+
query: "Record<string, never>",
|
|
2128
|
+
header: "Record<string, never>",
|
|
2129
|
+
params: "Record<string, never>"
|
|
2130
|
+
};
|
|
2131
|
+
group.operations.push({
|
|
2132
|
+
path,
|
|
2133
|
+
method: method.toUpperCase(),
|
|
2134
|
+
methodName: this.naming.deriveSdkMethodName(method, path, operation, options.methodStrategy ?? "smart"),
|
|
2135
|
+
summary: operation.summary,
|
|
2136
|
+
description: operation.description,
|
|
2137
|
+
operationId: operation.operationId,
|
|
2138
|
+
requestBodyDescription: operation.requestBody?.description,
|
|
2139
|
+
responseDescription: this.resolveSuccessResponseDescription(operation.responses),
|
|
2140
|
+
responseType: this.shapes.resolveSdkResponseType(operation.responses, refs.response),
|
|
2141
|
+
inputType: refs.input,
|
|
2142
|
+
queryType: refs.query,
|
|
2143
|
+
headerType: refs.header,
|
|
2144
|
+
paramsType: refs.params,
|
|
2145
|
+
hasBody: Boolean(operation.requestBody),
|
|
2146
|
+
bodyRequired: operation.requestBody?.required ?? false,
|
|
2147
|
+
pathParams: this.naming.createSdkParameterManifest(operation.parameters, "path", path),
|
|
2148
|
+
queryParams: this.naming.createSdkParameterManifest(operation.parameters, "query", path),
|
|
2149
|
+
headerParams: this.naming.createSdkParameterManifest(operation.parameters, "header", path)
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
groups.set(propertyName, group);
|
|
2153
|
+
}
|
|
2154
|
+
return { groups: Array.from(groups.values()).map((group) => ({
|
|
2155
|
+
...group,
|
|
2156
|
+
operations: this.naming.ensureUniqueSdkMethodNames(group.operations)
|
|
2157
|
+
})).sort((left, right) => left.propertyName.localeCompare(right.propertyName)) };
|
|
2158
|
+
}
|
|
2159
|
+
inferShapeFromExample(value, nameHint) {
|
|
2160
|
+
return this.shapes.inferShapeFromExample(value, nameHint);
|
|
2161
|
+
}
|
|
2162
|
+
sanitizeTypeName(value) {
|
|
2163
|
+
return this.naming.sanitizeTypeName(value);
|
|
2164
|
+
}
|
|
2165
|
+
registerNamedShape(shape, preferredName, context, collisionSuffix) {
|
|
2166
|
+
return this.shapes.registerNamedShape(shape, preferredName, context, collisionSuffix);
|
|
2167
|
+
}
|
|
2168
|
+
namespaceTopLevelShape(shape, role) {
|
|
2169
|
+
return this.shapes.namespaceTopLevelShape(shape, role);
|
|
2170
|
+
}
|
|
2171
|
+
registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias = false) {
|
|
2172
|
+
return this.shapes.registerObjectShape(shape, preferredName, context, collisionSuffix, emitAlias);
|
|
2173
|
+
}
|
|
2174
|
+
getOperationPriority(operation) {
|
|
2175
|
+
return Number(Boolean(operation.requestBody)) * 10;
|
|
2176
|
+
}
|
|
2177
|
+
resolveSuccessResponseDescription(responses) {
|
|
2178
|
+
for (const statusCode of [
|
|
2179
|
+
"200",
|
|
2180
|
+
"201",
|
|
2181
|
+
"202",
|
|
2182
|
+
"204"
|
|
2183
|
+
]) {
|
|
2184
|
+
const description = responses[statusCode]?.description?.trim();
|
|
2185
|
+
if (description) return description;
|
|
2186
|
+
}
|
|
2187
|
+
for (const response of Object.values(responses)) {
|
|
2188
|
+
const description = response.description?.trim();
|
|
2189
|
+
if (description) return description;
|
|
2190
|
+
}
|
|
2191
|
+
}
|
|
2192
|
+
};
|
|
2193
|
+
|
|
2194
|
+
//#endregion
|
|
2195
|
+
//#region src/generator/TypeScriptGenerator.ts
|
|
2196
|
+
var TypeScriptGenerator = class TypeScriptGenerator {
|
|
2197
|
+
typeBuilder = new TypeScriptTypeBuilder();
|
|
2198
|
+
moduleRenderer = new TypeScriptModuleRenderer();
|
|
2199
|
+
/**
|
|
2200
|
+
* Static helper method to generate a TypeScript module string from a generic JSON-like
|
|
2201
|
+
* value, inferring types and creating type definitions as needed, using the provided root
|
|
2202
|
+
* type name.
|
|
2203
|
+
*
|
|
2204
|
+
* @param value
|
|
2205
|
+
* @param rootTypeName
|
|
2206
|
+
* @returns
|
|
2207
|
+
*/
|
|
2208
|
+
static generateModule = (value, rootTypeName = "GeneratedOutput", options = {}) => {
|
|
2209
|
+
return new TypeScriptGenerator().generate(value, rootTypeName, options);
|
|
2210
|
+
};
|
|
2211
|
+
/**
|
|
2212
|
+
* Generate a TypeScript module string from a generic JSON-like value, inferring types
|
|
2213
|
+
* and creating type definitions as needed.
|
|
2214
|
+
*
|
|
2215
|
+
* @param value
|
|
2216
|
+
* @param rootTypeName
|
|
2217
|
+
* @returns
|
|
2218
|
+
*/
|
|
2219
|
+
generate(value, rootTypeName = "GeneratedOutput", options = {}) {
|
|
2220
|
+
if (this.isOpenApiDocumentLike(value)) return this.generateModule(value, rootTypeName, options);
|
|
2221
|
+
return this.generateGenericModule(value, rootTypeName);
|
|
2222
|
+
}
|
|
2223
|
+
/**
|
|
2224
|
+
* Generate a TypeScript module string from an OpenAPI document, including type definitions
|
|
2225
|
+
*
|
|
2226
|
+
* @param document
|
|
2227
|
+
* @param rootTypeName
|
|
2228
|
+
* @returns
|
|
2229
|
+
*/
|
|
2230
|
+
generateModule(document, rootTypeName, options = {}) {
|
|
2231
|
+
const context = this.typeBuilder.createContext();
|
|
2232
|
+
const operationTypeRefs = /* @__PURE__ */ new Map();
|
|
2233
|
+
for (const model of this.typeBuilder.collectSemanticModels(document)) {
|
|
2234
|
+
const operationKey = `${model.path}::${model.method}`;
|
|
2235
|
+
const resolvedName = this.typeBuilder.registerNamedShape(this.typeBuilder.namespaceTopLevelShape(model.shape, model.role), model.name, context, model.collisionSuffix);
|
|
2236
|
+
const existingRefs = operationTypeRefs.get(operationKey) ?? {
|
|
2237
|
+
response: "Record<string, never>",
|
|
2238
|
+
responseExample: "unknown",
|
|
2239
|
+
input: "Record<string, never>",
|
|
2240
|
+
query: "Record<string, never>",
|
|
2241
|
+
header: "Record<string, never>",
|
|
2242
|
+
params: "Record<string, never>"
|
|
2243
|
+
};
|
|
2244
|
+
existingRefs[model.role] = resolvedName;
|
|
2245
|
+
operationTypeRefs.set(operationKey, existingRefs);
|
|
2246
|
+
}
|
|
2247
|
+
const declarations = context.declarations.map((declaration) => this.moduleRenderer.renderDeclaration(declaration)).join("\n\n");
|
|
2248
|
+
const variableName = this.moduleRenderer.toCamelCase(rootTypeName);
|
|
2249
|
+
const sdkManifest = this.typeBuilder.buildSdkManifest(document, operationTypeRefs, options);
|
|
2250
|
+
return [
|
|
2251
|
+
declarations,
|
|
2252
|
+
this.moduleRenderer.renderOpenApiDocumentDefinitions(rootTypeName, document, operationTypeRefs),
|
|
2253
|
+
this.moduleRenderer.renderSdkApiInterface(rootTypeName, sdkManifest),
|
|
2254
|
+
this.moduleRenderer.renderSdkManifest(variableName, sdkManifest),
|
|
2255
|
+
`export const ${variableName}: ${rootTypeName} = ${this.moduleRenderer.renderOpenApiDocumentValue(document)}`,
|
|
2256
|
+
this.moduleRenderer.renderSdkBundle(variableName, rootTypeName),
|
|
2257
|
+
"",
|
|
2258
|
+
`export default ${variableName}`
|
|
2259
|
+
].filter(Boolean).join("\n\n");
|
|
2260
|
+
}
|
|
2261
|
+
generateGenericModule(value, rootTypeName) {
|
|
2262
|
+
const context = this.typeBuilder.createContext();
|
|
2263
|
+
const rootShape = this.typeBuilder.inferShapeFromExample(value, rootTypeName);
|
|
2264
|
+
const rootSanitizedName = this.typeBuilder.sanitizeTypeName(rootTypeName);
|
|
2265
|
+
let rootType = rootSanitizedName;
|
|
2266
|
+
if (rootShape.kind === "object") rootType = this.typeBuilder.registerObjectShape(rootShape, rootSanitizedName, context, rootSanitizedName);
|
|
2267
|
+
else {
|
|
2268
|
+
const declaration = {
|
|
2269
|
+
kind: "shape-alias",
|
|
2270
|
+
name: rootSanitizedName,
|
|
2271
|
+
shape: rootShape
|
|
2272
|
+
};
|
|
2273
|
+
context.declarations.push(declaration);
|
|
2274
|
+
context.declarationByName.set(rootSanitizedName, declaration);
|
|
2275
|
+
}
|
|
2276
|
+
const rootAlias = rootType === rootTypeName ? "" : `export type ${rootTypeName} = ${rootType}`;
|
|
2277
|
+
const variableName = this.moduleRenderer.toCamelCase(rootTypeName);
|
|
2278
|
+
return [
|
|
2279
|
+
context.declarations.map((declaration) => this.moduleRenderer.renderDeclaration(declaration)).join("\n\n"),
|
|
2280
|
+
rootAlias,
|
|
2281
|
+
`export const ${variableName}: ${rootTypeName} = ${this.moduleRenderer.renderValue(value)}`,
|
|
2282
|
+
"",
|
|
2283
|
+
`export default ${variableName}`
|
|
2284
|
+
].filter(Boolean).join("\n\n");
|
|
2285
|
+
}
|
|
2286
|
+
isOpenApiDocumentLike(value) {
|
|
2287
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
2288
|
+
const candidate = value;
|
|
2289
|
+
if (typeof candidate.info !== "object" || candidate.info === null || Array.isArray(candidate.info)) return false;
|
|
2290
|
+
const info = candidate.info;
|
|
2291
|
+
return candidate.openapi === "3.1.0" && typeof info.title === "string" && typeof info.version === "string" && typeof candidate.paths === "object" && candidate.paths !== null && !Array.isArray(candidate.paths);
|
|
2292
|
+
}
|
|
2293
|
+
};
|
|
2294
|
+
|
|
2295
|
+
//#endregion
|
|
2296
|
+
//#region src/generator/OutputGenerator.ts
|
|
2297
|
+
var OutputGenerator = class {
|
|
2298
|
+
/**
|
|
2299
|
+
* Serialize the extracted payload into the desired output format, optionally
|
|
2300
|
+
* generating TypeScript types when requested.
|
|
2301
|
+
*
|
|
2302
|
+
* @param payload The extracted payload to serialize.
|
|
2303
|
+
* @param outputFormat The desired output format ('json', 'js', 'ts', 'pretty').
|
|
2304
|
+
* @param rootTypeName The root type name to use when generating TypeScript types.
|
|
2305
|
+
* @returns A promise that resolves to the serialized output string.
|
|
2306
|
+
|
|
2307
|
+
*/
|
|
2308
|
+
static serializeOutput = async (payload, outputFormat, rootTypeName = "ExtractedApiDocument", typeScriptOptions = {}) => {
|
|
2309
|
+
if (outputFormat === "js") return prettier.default.format(`export default ${JSON.stringify(payload, null, 2)}`, {
|
|
2310
|
+
parser: "babel",
|
|
2311
|
+
semi: false,
|
|
2312
|
+
singleQuote: true
|
|
2313
|
+
});
|
|
2314
|
+
if (outputFormat === "ts") return prettier.default.format(TypeScriptGenerator.generateModule(payload, rootTypeName, typeScriptOptions), {
|
|
2315
|
+
parser: "typescript",
|
|
2316
|
+
semi: false,
|
|
2317
|
+
singleQuote: true
|
|
2318
|
+
});
|
|
2319
|
+
return JSON.stringify(payload, null, outputFormat === "json" ? 0 : 2);
|
|
2320
|
+
};
|
|
2321
|
+
/**
|
|
2322
|
+
* Build a safe file path for the output based on the workspace root, source
|
|
2323
|
+
* identifier, desired shape, and output format.
|
|
2324
|
+
*
|
|
2325
|
+
* @param workspaceRoot The root directory of the workspace.
|
|
2326
|
+
* @param source The original source identifier
|
|
2327
|
+
* @param shape The desired output shape ('raw' or 'openapi').
|
|
2328
|
+
* @param outputFormat The desired output format ('json', 'js', 'ts', 'pretty').
|
|
2329
|
+
* @returns The constructed file path for the output.
|
|
2330
|
+
*/
|
|
2331
|
+
static buildFilePath = (workspaceRoot, source, shape, outputFormat) => {
|
|
2332
|
+
const ext = {
|
|
2333
|
+
pretty: "txt",
|
|
2334
|
+
json: "json",
|
|
2335
|
+
js: "js",
|
|
2336
|
+
ts: "ts"
|
|
2337
|
+
}[outputFormat];
|
|
2338
|
+
const safeSource = this.toSafeSourceName(source);
|
|
2339
|
+
const shapeSuffix = shape === "openapi" ? ".openapi" : "";
|
|
2340
|
+
return node_path.default.join(workspaceRoot, "output", `${safeSource || "output"}${shapeSuffix}.${ext}`);
|
|
2341
|
+
};
|
|
2342
|
+
static buildArtifactDirectory = (workspaceRoot, source, artifact) => {
|
|
2343
|
+
const safeSource = this.toSafeSourceName(source);
|
|
2344
|
+
return node_path.default.join(workspaceRoot, "output", `${safeSource || artifact}.${artifact}`);
|
|
2345
|
+
};
|
|
2346
|
+
static toSafeSourceName(source) {
|
|
2347
|
+
return source.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "");
|
|
2348
|
+
}
|
|
2349
|
+
/**
|
|
2350
|
+
* Determine the appropriate root type name for TypeScript generation based on the desired
|
|
2351
|
+
* output shape. This helps ensure that generated types are meaningful and contextually relevant.
|
|
2352
|
+
*
|
|
2353
|
+
* @param shape The desired output shape ('raw' or 'openapi').
|
|
2354
|
+
* @returns The root type name to use for TypeScript generation.
|
|
2355
|
+
*/
|
|
2356
|
+
static getRootTypeName = (shape) => {
|
|
2357
|
+
return shape === "openapi" ? "ExtractedApiDocument" : "ExtractedPayload";
|
|
2358
|
+
};
|
|
2359
|
+
};
|
|
2360
|
+
|
|
2361
|
+
//#endregion
|
|
2362
|
+
//#region src/generator/SdkPackageGenerator.ts
|
|
2363
|
+
var SdkPackageGenerator = class {
|
|
2364
|
+
typeBuilder = new TypeScriptTypeBuilder();
|
|
2365
|
+
typeScriptGenerator = new TypeScriptGenerator();
|
|
2366
|
+
generate(document, options = {}) {
|
|
2367
|
+
const outputMode = options.outputMode ?? "both";
|
|
2368
|
+
const signatureStyle = options.signatureStyle ?? "grouped";
|
|
2369
|
+
const rootTypeName = options.rootTypeName ?? "ExtractedApiDocument";
|
|
2370
|
+
const schemaModule = options.schemaModule ?? this.typeScriptGenerator.generateModule(document, rootTypeName, options);
|
|
2371
|
+
const operationTypeRefs = this.createOperationTypeRefs(document);
|
|
2372
|
+
const manifest = this.typeBuilder.buildSdkManifest(document, operationTypeRefs, options);
|
|
2373
|
+
const classNames = manifest.groups.map((group) => group.className);
|
|
2374
|
+
const files = {
|
|
2375
|
+
"package.json": this.renderPackageJson(options),
|
|
2376
|
+
"README.md": this.renderReadme(manifest, options, outputMode, signatureStyle),
|
|
2377
|
+
"src/Schema.ts": schemaModule,
|
|
2378
|
+
"src/index.ts": this.renderIndexFile(classNames, outputMode, rootTypeName),
|
|
2379
|
+
"tsconfig.json": this.renderTsconfig(),
|
|
2380
|
+
"tsdown.config.ts": this.renderTsdownConfig(),
|
|
2381
|
+
"vitest.config.ts": this.renderVitestConfig(),
|
|
2382
|
+
"tests/exports.test.ts": this.renderExportsTest(rootTypeName, outputMode)
|
|
2383
|
+
};
|
|
2384
|
+
if (outputMode !== "runtime") {
|
|
2385
|
+
files["src/BaseApi.ts"] = this.renderBaseApi();
|
|
2386
|
+
files["src/ApiBinder.ts"] = this.renderApiBinder(manifest);
|
|
2387
|
+
for (const group of manifest.groups) files[`src/Apis/${group.className}.ts`] = this.renderApiClass(group, signatureStyle);
|
|
2388
|
+
files["src/Core.ts"] = this.renderCoreFile();
|
|
2389
|
+
}
|
|
2390
|
+
return files;
|
|
2391
|
+
}
|
|
2392
|
+
createOperationTypeRefs(document) {
|
|
2393
|
+
const context = this.typeBuilder.createContext();
|
|
2394
|
+
const operationTypeRefs = /* @__PURE__ */ new Map();
|
|
2395
|
+
for (const model of this.typeBuilder.collectSemanticModels(document)) {
|
|
2396
|
+
const operationKey = `${model.path}::${model.method}`;
|
|
2397
|
+
const resolvedName = this.typeBuilder.registerNamedShape(this.typeBuilder.namespaceTopLevelShape(model.shape, model.role), model.name, context, model.collisionSuffix);
|
|
2398
|
+
const existingRefs = operationTypeRefs.get(operationKey) ?? {
|
|
2399
|
+
response: "Record<string, never>",
|
|
2400
|
+
responseExample: "unknown",
|
|
2401
|
+
input: "Record<string, never>",
|
|
2402
|
+
query: "Record<string, never>",
|
|
2403
|
+
header: "Record<string, never>",
|
|
2404
|
+
params: "Record<string, never>"
|
|
2405
|
+
};
|
|
2406
|
+
existingRefs[model.role] = resolvedName;
|
|
2407
|
+
operationTypeRefs.set(operationKey, existingRefs);
|
|
2408
|
+
}
|
|
2409
|
+
return operationTypeRefs;
|
|
2410
|
+
}
|
|
2411
|
+
renderBaseApi() {
|
|
2412
|
+
return [
|
|
2413
|
+
"import { BaseApi as KitBaseApi } from '@oapiex/sdk-kit'",
|
|
2414
|
+
"",
|
|
2415
|
+
"export class BaseApi extends KitBaseApi {}"
|
|
2416
|
+
].join("\n");
|
|
2417
|
+
}
|
|
2418
|
+
renderApiBinder(manifest) {
|
|
2419
|
+
return [
|
|
2420
|
+
"import { BaseApi } from './BaseApi'",
|
|
2421
|
+
"",
|
|
2422
|
+
...manifest.groups.map((group) => `import { ${group.className} } from './Apis/${group.className}'`),
|
|
2423
|
+
"",
|
|
2424
|
+
"export class ApiBinder extends BaseApi {",
|
|
2425
|
+
...manifest.groups.map((group) => ` ${group.propertyName}!: ${group.className}`),
|
|
2426
|
+
"",
|
|
2427
|
+
" protected override boot () {",
|
|
2428
|
+
...manifest.groups.map((group) => ` this.${group.propertyName} = new ${group.className}(this.core)`),
|
|
2429
|
+
" }",
|
|
2430
|
+
"}"
|
|
2431
|
+
].join("\n");
|
|
2432
|
+
}
|
|
2433
|
+
renderApiClass(group, signatureStyle) {
|
|
2434
|
+
const typeImportContext = this.createTypeImportContext(group, signatureStyle);
|
|
2435
|
+
const imports = ["import { BaseApi } from '../BaseApi'", "import { Http } from '@oapiex/sdk-kit'"];
|
|
2436
|
+
if (typeImportContext.specifiers.length > 0) imports.splice(1, 0, `import type { ${typeImportContext.specifiers.join(", ")} } from '../Schema'`);
|
|
2437
|
+
return [
|
|
2438
|
+
...imports,
|
|
2439
|
+
"",
|
|
2440
|
+
`export class ${group.className} extends BaseApi {`,
|
|
2441
|
+
"",
|
|
2442
|
+
...group.operations.flatMap((operation) => [this.renderApiMethod(operation, signatureStyle, typeImportContext.aliasMap), ""]).slice(0, -1),
|
|
2443
|
+
"}"
|
|
2444
|
+
].join("\n");
|
|
2445
|
+
}
|
|
2446
|
+
renderApiMethod(operation, signatureStyle, aliasMap) {
|
|
2447
|
+
const signature = signatureStyle === "flat" ? this.renderFlatSignature(operation, aliasMap) : this.renderGroupedSignature(operation, aliasMap);
|
|
2448
|
+
const urlPathArgs = signatureStyle === "flat" ? this.renderFlatObjectLiteral(operation.paramsType, operation.pathParams) : operation.pathParams.length > 0 ? "params" : "{}";
|
|
2449
|
+
const urlQueryArgs = signatureStyle === "flat" ? this.renderFlatObjectLiteral(operation.queryType, operation.queryParams) : operation.queryParams.length > 0 ? "query" : "{}";
|
|
2450
|
+
const headerArgs = signatureStyle === "flat" ? this.renderFlatHeaders(operation) : operation.headerParams.length > 0 ? "((headers ? { ...headers } : {}) as Record<string, string | undefined>)" : "{}";
|
|
2451
|
+
const bodyArg = signatureStyle === "flat" ? operation.hasBody ? "body" : "{}" : operation.hasBody ? "body ?? {}" : "{}";
|
|
2452
|
+
const docComment = this.renderMethodDocComment(operation, signatureStyle, aliasMap);
|
|
2453
|
+
return [
|
|
2454
|
+
...docComment ? [docComment] : [],
|
|
2455
|
+
` async ${operation.methodName} ${signature}: Promise<${this.rewriteTypeReference(operation.responseType, aliasMap)}> {`,
|
|
2456
|
+
" await this.core.validateAccess()",
|
|
2457
|
+
"",
|
|
2458
|
+
` const { data } = await Http.send<${this.rewriteTypeReference(operation.responseType, aliasMap)}>(`,
|
|
2459
|
+
` this.core.builder.buildTargetUrl('${operation.path}', ${urlPathArgs}, ${urlQueryArgs}),`,
|
|
2460
|
+
` '${operation.method}',`,
|
|
2461
|
+
` ${bodyArg},`,
|
|
2462
|
+
` ${headerArgs}`,
|
|
2463
|
+
" )",
|
|
2464
|
+
"",
|
|
2465
|
+
" return data",
|
|
2466
|
+
" }"
|
|
2467
|
+
].join("\n");
|
|
2468
|
+
}
|
|
2469
|
+
renderMethodDocComment(operation, signatureStyle, aliasMap) {
|
|
2470
|
+
const lines = [];
|
|
2471
|
+
const summary = operation.summary?.trim();
|
|
2472
|
+
const description = operation.description?.trim();
|
|
2473
|
+
const operationId = operation.operationId?.trim();
|
|
2474
|
+
const responseType = this.rewriteTypeReference(operation.responseType, aliasMap);
|
|
2475
|
+
const responseDescription = operation.responseDescription?.trim();
|
|
2476
|
+
if (summary) lines.push(summary);
|
|
2477
|
+
if (description && description !== summary) {
|
|
2478
|
+
if (lines.length > 0) lines.push("");
|
|
2479
|
+
lines.push(...this.wrapDocText(description));
|
|
2480
|
+
}
|
|
2481
|
+
const metadataLines = [`HTTP ${operation.method} ${operation.path}`, ...operationId ? [`Operation ID: ${operationId}`] : []];
|
|
2482
|
+
if (metadataLines.length > 0) {
|
|
2483
|
+
if (lines.length > 0) lines.push("");
|
|
2484
|
+
lines.push(...metadataLines);
|
|
2485
|
+
}
|
|
2486
|
+
const parameterDocs = signatureStyle === "flat" ? this.renderFlatParameterDocs(operation, aliasMap) : this.renderGroupedParameterDocs(operation, aliasMap);
|
|
2487
|
+
if (parameterDocs.length > 0) {
|
|
2488
|
+
if (lines.length > 0) lines.push("");
|
|
2489
|
+
lines.push(...parameterDocs);
|
|
2490
|
+
}
|
|
2491
|
+
lines.push(`@returns ${responseDescription ? `${responseDescription} ` : ""}${responseType}`.trim());
|
|
2492
|
+
return [
|
|
2493
|
+
" /**",
|
|
2494
|
+
...lines.map((line) => line ? ` * ${line}` : " *"),
|
|
2495
|
+
" */"
|
|
2496
|
+
].join("\n");
|
|
2497
|
+
}
|
|
2498
|
+
renderGroupedParameterDocs(operation, aliasMap) {
|
|
2499
|
+
const docs = [];
|
|
2500
|
+
if (operation.pathParams.length > 0) docs.push(this.renderParamDoc("params", operation.paramsType, aliasMap, this.describeParameterGroup(operation.pathParams, "path parameters")));
|
|
2501
|
+
if (operation.queryParams.length > 0) docs.push(this.renderParamDoc("query", operation.queryType, aliasMap, this.describeParameterGroup(operation.queryParams, "query parameters")));
|
|
2502
|
+
if (operation.hasBody) docs.push(this.renderParamDoc("body", operation.inputType, aliasMap, operation.requestBodyDescription?.trim() || "Request body"));
|
|
2503
|
+
if (operation.headerParams.length > 0) docs.push(this.renderParamDoc("headers", operation.headerType, aliasMap, this.describeParameterGroup(operation.headerParams, "request headers")));
|
|
2504
|
+
return docs;
|
|
2505
|
+
}
|
|
2506
|
+
renderFlatParameterDocs(operation, aliasMap) {
|
|
2507
|
+
return [
|
|
2508
|
+
...operation.pathParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.paramsType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Path parameter ${parameter.name}`)),
|
|
2509
|
+
...operation.queryParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.queryType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Query parameter ${parameter.name}`)),
|
|
2510
|
+
...operation.hasBody ? [this.renderParamDoc("body", operation.inputType, aliasMap, operation.requestBodyDescription?.trim() || "Request body")] : [],
|
|
2511
|
+
...operation.headerParams.map((parameter) => this.renderParamDoc(parameter.accessor, `${operation.headerType}[${JSON.stringify(parameter.name)}]`, aliasMap, parameter.description?.trim() || `Header ${parameter.name}`))
|
|
2512
|
+
];
|
|
2513
|
+
}
|
|
2514
|
+
renderParamDoc(name, typeRef, aliasMap, description) {
|
|
2515
|
+
return `@param ${name} ${description} Type: ${this.rewriteTypeReference(typeRef, aliasMap)}`;
|
|
2516
|
+
}
|
|
2517
|
+
describeParameterGroup(parameters, fallback) {
|
|
2518
|
+
const described = parameters.map((parameter) => parameter.description?.trim() ? `${parameter.name}: ${parameter.description.trim()}` : parameter.name).filter(Boolean);
|
|
2519
|
+
if (described.length === 0) return fallback;
|
|
2520
|
+
return described.join("; ");
|
|
2521
|
+
}
|
|
2522
|
+
wrapDocText(text) {
|
|
2523
|
+
return text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
2524
|
+
}
|
|
2525
|
+
renderGroupedSignature(operation, aliasMap) {
|
|
2526
|
+
const args = [];
|
|
2527
|
+
if (operation.pathParams.length > 0) args.push(`params: ${this.rewriteTypeReference(operation.paramsType, aliasMap)}`);
|
|
2528
|
+
if (operation.queryParams.length > 0) args.push(`query: ${this.rewriteTypeReference(operation.queryType, aliasMap)}`);
|
|
2529
|
+
if (operation.hasBody) args.push(`body${operation.bodyRequired ? "" : "?"}: ${this.rewriteTypeReference(operation.inputType, aliasMap)}`);
|
|
2530
|
+
if (operation.headerParams.length > 0) args.push(`headers?: ${this.rewriteTypeReference(operation.headerType, aliasMap)}`);
|
|
2531
|
+
return `(${args.join(", ")})`;
|
|
2532
|
+
}
|
|
2533
|
+
renderFlatSignature(operation, aliasMap) {
|
|
2534
|
+
return `(${[
|
|
2535
|
+
...operation.pathParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.paramsType, aliasMap)}[${JSON.stringify(parameter.name)}]`),
|
|
2536
|
+
...operation.queryParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.queryType, aliasMap)}[${JSON.stringify(parameter.name)}]`),
|
|
2537
|
+
...operation.hasBody ? [`body${operation.bodyRequired ? "" : "?"}: ${this.rewriteTypeReference(operation.inputType, aliasMap)}`] : [],
|
|
2538
|
+
...operation.headerParams.map((parameter) => `${parameter.accessor}${parameter.required ? "" : "?"}: ${this.rewriteTypeReference(operation.headerType, aliasMap)}[${JSON.stringify(parameter.name)}]`)
|
|
2539
|
+
].join(", ")})`;
|
|
2540
|
+
}
|
|
2541
|
+
createTypeImportContext(group, signatureStyle) {
|
|
2542
|
+
const requiredTypeRefs = /* @__PURE__ */ new Set();
|
|
2543
|
+
for (const operation of group.operations) {
|
|
2544
|
+
requiredTypeRefs.add(operation.responseType);
|
|
2545
|
+
if (operation.hasBody) requiredTypeRefs.add(operation.inputType);
|
|
2546
|
+
if (operation.queryParams.length > 0) requiredTypeRefs.add(operation.queryType);
|
|
2547
|
+
if (operation.headerParams.length > 0) requiredTypeRefs.add(operation.headerType);
|
|
2548
|
+
if (operation.pathParams.length > 0) requiredTypeRefs.add(operation.paramsType);
|
|
2549
|
+
if (signatureStyle === "flat") continue;
|
|
2550
|
+
}
|
|
2551
|
+
const identifiers = Array.from(new Set(Array.from(requiredTypeRefs).flatMap((typeRef) => this.collectTypeIdentifiers(typeRef)))).sort();
|
|
2552
|
+
const aliasMap = /* @__PURE__ */ new Map();
|
|
2553
|
+
return {
|
|
2554
|
+
specifiers: identifiers.map((identifier) => {
|
|
2555
|
+
if (identifier === group.className) {
|
|
2556
|
+
const aliasedName = `${identifier}Model`;
|
|
2557
|
+
aliasMap.set(identifier, aliasedName);
|
|
2558
|
+
return `${identifier} as ${aliasedName}`;
|
|
2559
|
+
}
|
|
2560
|
+
return identifier;
|
|
2561
|
+
}),
|
|
2562
|
+
aliasMap
|
|
2563
|
+
};
|
|
2564
|
+
}
|
|
2565
|
+
rewriteTypeReference(typeRef, aliasMap) {
|
|
2566
|
+
let rewritten = typeRef;
|
|
2567
|
+
for (const [identifier, alias] of aliasMap.entries()) rewritten = rewritten.replace(new RegExp(`\\b${identifier}\\b`, "g"), alias);
|
|
2568
|
+
return rewritten;
|
|
2569
|
+
}
|
|
2570
|
+
renderFlatObjectLiteral(_typeName, parameters) {
|
|
2571
|
+
if (parameters.length === 0) return "{}";
|
|
2572
|
+
return `{ ${parameters.map((parameter) => `${JSON.stringify(parameter.name)}: ${parameter.accessor}`).join(", ")} }`;
|
|
2573
|
+
}
|
|
2574
|
+
renderFlatHeaders(operation) {
|
|
2575
|
+
if (operation.headerParams.length === 0) return "{}";
|
|
2576
|
+
return `({ ${operation.headerParams.map((parameter) => `${JSON.stringify(parameter.name)}: ${parameter.accessor}`).join(", ")} } as Record<string, string | undefined>)`;
|
|
2577
|
+
}
|
|
2578
|
+
renderCoreFile() {
|
|
2579
|
+
return [
|
|
2580
|
+
"import { Core as KitCore } from '@oapiex/sdk-kit'",
|
|
2581
|
+
"",
|
|
2582
|
+
"import { ApiBinder } from './ApiBinder'",
|
|
2583
|
+
"",
|
|
2584
|
+
"export class Core extends KitCore {",
|
|
2585
|
+
" static override apiClass = ApiBinder",
|
|
2586
|
+
"",
|
|
2587
|
+
" declare api: ApiBinder",
|
|
2588
|
+
"}"
|
|
2589
|
+
].join("\n");
|
|
2590
|
+
}
|
|
2591
|
+
renderPackageJson(options) {
|
|
2592
|
+
return JSON.stringify({
|
|
2593
|
+
name: options.packageName ?? "generated-sdk",
|
|
2594
|
+
type: "module",
|
|
2595
|
+
version: options.packageVersion ?? "0.1.0",
|
|
2596
|
+
private: true,
|
|
2597
|
+
description: options.packageDescription ?? "Generated SDK scaffold emitted by oapiex.",
|
|
2598
|
+
main: "./dist/index.cjs",
|
|
2599
|
+
module: "./dist/index.js",
|
|
2600
|
+
types: "./dist/index.d.ts",
|
|
2601
|
+
exports: {
|
|
2602
|
+
".": {
|
|
2603
|
+
import: "./dist/index.js",
|
|
2604
|
+
require: "./dist/index.cjs"
|
|
2605
|
+
},
|
|
2606
|
+
"./package.json": "./package.json"
|
|
2607
|
+
},
|
|
2608
|
+
files: ["dist"],
|
|
2609
|
+
scripts: {
|
|
2610
|
+
test: "pnpm vitest --run",
|
|
2611
|
+
"test:watch": "pnpm vitest",
|
|
2612
|
+
build: "tsdown"
|
|
2613
|
+
},
|
|
2614
|
+
dependencies: { [options.sdkKitPackageName ?? "@oapiex/sdk-kit"]: "^0.1.1" },
|
|
2615
|
+
devDependencies: {
|
|
2616
|
+
"@types/node": "^20.14.5",
|
|
2617
|
+
tsdown: "^0.20.1",
|
|
2618
|
+
typescript: "^5.4.5",
|
|
2619
|
+
vitest: "^3.2.4"
|
|
2620
|
+
}
|
|
2621
|
+
}, null, 2);
|
|
2622
|
+
}
|
|
2623
|
+
renderReadme(manifest, options, outputMode, signatureStyle) {
|
|
2624
|
+
const packageName = options.packageName ?? "generated-sdk";
|
|
2625
|
+
const title = `# ${packageName}`;
|
|
2626
|
+
const description = this.renderReadmeDescription(outputMode);
|
|
2627
|
+
const usage = this.renderReadmeUsage(manifest, packageName, outputMode, signatureStyle);
|
|
2628
|
+
const exports = this.renderReadmeExports(outputMode);
|
|
2629
|
+
return [
|
|
2630
|
+
title,
|
|
2631
|
+
"",
|
|
2632
|
+
description,
|
|
2633
|
+
"",
|
|
2634
|
+
"## Install",
|
|
2635
|
+
"",
|
|
2636
|
+
"```bash",
|
|
2637
|
+
`pnpm add ${packageName}`,
|
|
2638
|
+
"```",
|
|
2639
|
+
"",
|
|
2640
|
+
"## Quick Start",
|
|
2641
|
+
"",
|
|
2642
|
+
"```ts",
|
|
2643
|
+
usage,
|
|
2644
|
+
"```",
|
|
2645
|
+
"",
|
|
2646
|
+
"## Main Exports",
|
|
2647
|
+
"",
|
|
2648
|
+
...exports.map((line) => `- ${line}`),
|
|
2649
|
+
"",
|
|
2650
|
+
"## Commands",
|
|
2651
|
+
"",
|
|
2652
|
+
"```bash",
|
|
2653
|
+
"pnpm test",
|
|
2654
|
+
"pnpm build",
|
|
2655
|
+
"```"
|
|
2656
|
+
].join("\n");
|
|
2657
|
+
}
|
|
2658
|
+
renderReadmeDescription(outputMode) {
|
|
2659
|
+
if (outputMode === "runtime") return "Generated runtime-first TypeScript SDK emitted by oapiex.";
|
|
2660
|
+
if (outputMode === "classes") return "Generated class-based TypeScript SDK emitted by oapiex.";
|
|
2661
|
+
return "Generated TypeScript SDK emitted by oapiex with both class-based and runtime-first entrypoints.";
|
|
2662
|
+
}
|
|
2663
|
+
renderReadmeUsage(manifest, packageName, outputMode, signatureStyle) {
|
|
2664
|
+
const exampleOperation = this.pickExampleOperation(manifest);
|
|
2665
|
+
const runtimeSnippet = this.renderReadmeClientSnippet(packageName, "runtime", signatureStyle, exampleOperation);
|
|
2666
|
+
if (outputMode === "runtime") return runtimeSnippet;
|
|
2667
|
+
const classSnippet = this.renderReadmeClientSnippet(packageName, "classes", signatureStyle, exampleOperation);
|
|
2668
|
+
if (outputMode === "classes") return classSnippet;
|
|
2669
|
+
const typeImports = exampleOperation ? this.collectReadmeTypeImports(exampleOperation.operation) : [];
|
|
2670
|
+
return [
|
|
2671
|
+
typeImports.length > 0 ? `import { Core, createClient, type ${typeImports.join(", type ")} } from '${packageName}'` : `import { Core, createClient } from '${packageName}'`,
|
|
2672
|
+
"",
|
|
2673
|
+
...this.renderReadmeClientBody("sdk", "classes", signatureStyle, exampleOperation),
|
|
2674
|
+
"",
|
|
2675
|
+
"// --- OR ---",
|
|
2676
|
+
"",
|
|
2677
|
+
...this.renderReadmeClientBody("runtimeSdk", "runtime", signatureStyle, exampleOperation)
|
|
2678
|
+
].join("\n");
|
|
2679
|
+
}
|
|
2680
|
+
renderReadmeClientSnippet(packageName, mode, signatureStyle, exampleOperation) {
|
|
2681
|
+
const importNames = mode === "runtime" ? ["createClient"] : ["Core"];
|
|
2682
|
+
const typeImports = exampleOperation ? this.collectReadmeTypeImports(exampleOperation.operation) : [];
|
|
2683
|
+
const importLine = typeImports.length > 0 ? `import { ${importNames.join(", ")}, type ${typeImports.join(", type ")} } from '${packageName}'` : `import { ${importNames.join(", ")} } from '${packageName}'`;
|
|
2684
|
+
const sdkVariable = mode === "runtime" ? "runtimeSdk" : "sdk";
|
|
2685
|
+
return [
|
|
2686
|
+
importLine,
|
|
2687
|
+
"",
|
|
2688
|
+
...this.renderReadmeClientBody(sdkVariable, mode, signatureStyle, exampleOperation)
|
|
2689
|
+
].join("\n");
|
|
2690
|
+
}
|
|
2691
|
+
renderReadmeClientBody(sdkVariable, mode, signatureStyle, exampleOperation) {
|
|
2692
|
+
const initLine = mode === "runtime" ? `const ${sdkVariable} = createClient({` : `const ${sdkVariable} = new Core({`;
|
|
2693
|
+
const callLines = exampleOperation ? this.renderReadmeOperationCall(sdkVariable, exampleOperation, mode === "runtime" ? "grouped" : signatureStyle) : [];
|
|
2694
|
+
return [
|
|
2695
|
+
initLine,
|
|
2696
|
+
" clientId: process.env.CLIENT_ID!,",
|
|
2697
|
+
" clientSecret: process.env.CLIENT_SECRET!,",
|
|
2698
|
+
" environment: 'sandbox',",
|
|
2699
|
+
"})",
|
|
2700
|
+
...callLines.length > 0 ? ["", ...callLines] : []
|
|
2701
|
+
];
|
|
2702
|
+
}
|
|
2703
|
+
renderReadmeExports(outputMode) {
|
|
2704
|
+
if (outputMode === "runtime") return ["`createClient()` for a typed runtime SDK instance", "`Schema` exports for request, response, params, query, and header types"];
|
|
2705
|
+
if (outputMode === "classes") return ["`Core` as the class-based SDK entrypoint", "generated API classes plus `Schema` type exports"];
|
|
2706
|
+
return [
|
|
2707
|
+
"`Core` for class-based usage",
|
|
2708
|
+
"`createClient()` for runtime-first usage",
|
|
2709
|
+
"`Schema` exports for generated request, response, params, query, and header types"
|
|
2710
|
+
];
|
|
2711
|
+
}
|
|
2712
|
+
pickExampleOperation(manifest) {
|
|
2713
|
+
const group = manifest.groups[0];
|
|
2714
|
+
const operation = group?.operations[0];
|
|
2715
|
+
if (!group || !operation) return null;
|
|
2716
|
+
return {
|
|
2717
|
+
group,
|
|
2718
|
+
operation
|
|
2719
|
+
};
|
|
2720
|
+
}
|
|
2721
|
+
collectReadmeTypeImports(operation) {
|
|
2722
|
+
const types = /* @__PURE__ */ new Set();
|
|
2723
|
+
if (operation.pathParams.length > 0) types.add(operation.paramsType);
|
|
2724
|
+
if (operation.queryParams.length > 0) types.add(operation.queryType);
|
|
2725
|
+
if (operation.hasBody) types.add(operation.inputType);
|
|
2726
|
+
if (operation.headerParams.length > 0) types.add(operation.headerType);
|
|
2727
|
+
return Array.from(types).sort();
|
|
2728
|
+
}
|
|
2729
|
+
renderReadmeOperationCall(sdkVariable, exampleOperation, signatureStyle) {
|
|
2730
|
+
const { group, operation } = exampleOperation;
|
|
2731
|
+
const args = signatureStyle === "flat" ? this.renderReadmeFlatArgs(operation) : this.renderReadmeGroupedArgs(operation);
|
|
2732
|
+
return [
|
|
2733
|
+
`await ${sdkVariable}.api.${group.propertyName}.${operation.methodName}(`,
|
|
2734
|
+
...args.map((arg) => ` ${arg},`),
|
|
2735
|
+
")"
|
|
2736
|
+
];
|
|
2737
|
+
}
|
|
2738
|
+
renderReadmeGroupedArgs(operation) {
|
|
2739
|
+
const args = [];
|
|
2740
|
+
if (operation.pathParams.length > 0) args.push(`{} as ${operation.paramsType}`);
|
|
2741
|
+
if (operation.queryParams.length > 0) args.push(`{} as ${operation.queryType}`);
|
|
2742
|
+
if (operation.hasBody) args.push(`{} as ${operation.inputType}`);
|
|
2743
|
+
if (operation.headerParams.length > 0) args.push(`{} as ${operation.headerType}`);
|
|
2744
|
+
return args;
|
|
2745
|
+
}
|
|
2746
|
+
renderReadmeFlatArgs(operation) {
|
|
2747
|
+
return [
|
|
2748
|
+
...operation.pathParams.map((parameter) => `{} as ${operation.paramsType}[${JSON.stringify(parameter.name)}]`),
|
|
2749
|
+
...operation.queryParams.map((parameter) => `{} as ${operation.queryType}[${JSON.stringify(parameter.name)}]`),
|
|
2750
|
+
...operation.hasBody ? [`{} as ${operation.inputType}`] : [],
|
|
2751
|
+
...operation.headerParams.map((parameter) => `{} as ${operation.headerType}[${JSON.stringify(parameter.name)}]`)
|
|
2752
|
+
];
|
|
2753
|
+
}
|
|
2754
|
+
renderTsconfig() {
|
|
2755
|
+
return JSON.stringify({
|
|
2756
|
+
compilerOptions: {
|
|
2757
|
+
rootDir: ".",
|
|
2758
|
+
outDir: "./dist",
|
|
2759
|
+
target: "esnext",
|
|
2760
|
+
module: "es2022",
|
|
2761
|
+
moduleResolution: "bundler",
|
|
2762
|
+
esModuleInterop: true,
|
|
2763
|
+
strict: true,
|
|
2764
|
+
allowJs: true,
|
|
2765
|
+
skipLibCheck: true,
|
|
2766
|
+
resolveJsonModule: true
|
|
2767
|
+
},
|
|
2768
|
+
include: ["./src/**/*", "./tests/**/*"],
|
|
2769
|
+
exclude: ["./dist", "./node_modules"]
|
|
2770
|
+
}, null, 2);
|
|
2771
|
+
}
|
|
2772
|
+
renderTsdownConfig() {
|
|
2773
|
+
return [
|
|
2774
|
+
"import { defineConfig } from 'tsdown'",
|
|
2775
|
+
"",
|
|
2776
|
+
"export default defineConfig({",
|
|
2777
|
+
" entry: {",
|
|
2778
|
+
" index: 'src/index.ts',",
|
|
2779
|
+
" },",
|
|
2780
|
+
" exports: true,",
|
|
2781
|
+
" format: ['esm', 'cjs'],",
|
|
2782
|
+
" outDir: 'dist',",
|
|
2783
|
+
" dts: true,",
|
|
2784
|
+
" sourcemap: false,",
|
|
2785
|
+
" external: ['@oapiex/sdk-kit'],",
|
|
2786
|
+
" clean: true,",
|
|
2787
|
+
"})"
|
|
2788
|
+
].join("\n");
|
|
2789
|
+
}
|
|
2790
|
+
renderVitestConfig() {
|
|
2791
|
+
return [
|
|
2792
|
+
"import { defineConfig } from 'vitest/config'",
|
|
2793
|
+
"",
|
|
2794
|
+
"export default defineConfig({",
|
|
2795
|
+
" test: {",
|
|
2796
|
+
" name: 'generated-sdk',",
|
|
2797
|
+
" environment: 'node',",
|
|
2798
|
+
" include: ['tests/*.{test,spec}.?(c|m)[jt]s?(x)'],",
|
|
2799
|
+
" },",
|
|
2800
|
+
"})"
|
|
2801
|
+
].join("\n");
|
|
2802
|
+
}
|
|
2803
|
+
renderExportsTest(rootTypeName, outputMode) {
|
|
2804
|
+
const rootExportName = `${rootTypeName.charAt(0).toLowerCase()}${rootTypeName.slice(1)}`;
|
|
2805
|
+
const assertions = [
|
|
2806
|
+
" expect(sdk.createClient).toBeTypeOf('function')",
|
|
2807
|
+
" expect(sdk.createSdk).toBeTypeOf('function')",
|
|
2808
|
+
` expect(sdk.${rootExportName}Sdk).toBeDefined()`,
|
|
2809
|
+
` expect(sdk.${rootExportName}Manifest).toBeDefined()`
|
|
2810
|
+
];
|
|
2811
|
+
if (outputMode !== "runtime") {
|
|
2812
|
+
assertions.unshift(" expect(sdk.Core).toBeTypeOf('function')");
|
|
2813
|
+
assertions.unshift(" expect(sdk.BaseApi).toBeTypeOf('function')");
|
|
2814
|
+
}
|
|
2815
|
+
return [
|
|
2816
|
+
"import { describe, expect, it } from 'vitest'",
|
|
2817
|
+
"",
|
|
2818
|
+
"import * as sdk from '../src/index'",
|
|
2819
|
+
"",
|
|
2820
|
+
"describe('generated sdk exports', () => {",
|
|
2821
|
+
" it('exposes the generated schema and runtime helpers', () => {",
|
|
2822
|
+
...assertions,
|
|
2823
|
+
" })",
|
|
2824
|
+
"})"
|
|
2825
|
+
].join("\n");
|
|
2826
|
+
}
|
|
2827
|
+
renderIndexFile(classNames, outputMode, rootTypeName) {
|
|
2828
|
+
const rootExportName = `${rootTypeName.charAt(0).toLowerCase()}${rootTypeName.slice(1)}`;
|
|
2829
|
+
const lines = [
|
|
2830
|
+
`import type { ${rootTypeName}Api } from './Schema'`,
|
|
2831
|
+
`import { ${rootExportName}Sdk } from './Schema'`,
|
|
2832
|
+
"import { createSdk as createBoundSdk } from '@oapiex/sdk-kit'",
|
|
2833
|
+
"import type { BaseApi as KitBaseApi, Core as KitCore, InitOptions } from '@oapiex/sdk-kit'",
|
|
2834
|
+
"",
|
|
2835
|
+
"export * from './Schema'"
|
|
2836
|
+
];
|
|
2837
|
+
if (outputMode !== "runtime") {
|
|
2838
|
+
lines.push("export { ApiBinder } from './ApiBinder'");
|
|
2839
|
+
lines.push("export { BaseApi } from './BaseApi'");
|
|
2840
|
+
lines.push(...classNames.map((className) => `export { ${className} as ${className}Api } from './Apis/${className}'`));
|
|
2841
|
+
lines.push("export { Core } from './Core'");
|
|
2842
|
+
}
|
|
2843
|
+
lines.push("");
|
|
2844
|
+
lines.push("export const createClient = (");
|
|
2845
|
+
lines.push(" options: InitOptions");
|
|
2846
|
+
lines.push(`): KitCore & { api: KitBaseApi & ${rootTypeName}Api } =>`);
|
|
2847
|
+
lines.push(` createBoundSdk(${rootExportName}Sdk, options) as KitCore & { api: KitBaseApi & ${rootTypeName}Api }`);
|
|
2848
|
+
lines.push("");
|
|
2849
|
+
lines.push("export {");
|
|
2850
|
+
lines.push(" BadRequestException,");
|
|
2851
|
+
lines.push(" Builder,");
|
|
2852
|
+
lines.push(" ForbiddenRequestException,");
|
|
2853
|
+
lines.push(" Http,");
|
|
2854
|
+
lines.push(" HttpException,");
|
|
2855
|
+
lines.push(" UnauthorizedRequestException,");
|
|
2856
|
+
lines.push(" WebhookValidator,");
|
|
2857
|
+
lines.push(" createSdk,");
|
|
2858
|
+
lines.push("} from '@oapiex/sdk-kit'");
|
|
2859
|
+
lines.push("");
|
|
2860
|
+
lines.push("export type {");
|
|
2861
|
+
lines.push(" InitOptions,");
|
|
2862
|
+
lines.push(" UnifiedResponse,");
|
|
2863
|
+
lines.push(" XGenericObject,");
|
|
2864
|
+
lines.push("} from '@oapiex/sdk-kit'");
|
|
2865
|
+
return lines.join("\n");
|
|
2866
|
+
}
|
|
2867
|
+
collectTypeIdentifiers(typeRef) {
|
|
2868
|
+
return Array.from(new Set((typeRef.match(/\b[A-Z][A-Za-z0-9_]*/g) ?? []).filter((identifier) => {
|
|
2869
|
+
return !["Record", "Promise"].includes(identifier);
|
|
2870
|
+
})));
|
|
2871
|
+
}
|
|
2872
|
+
};
|
|
2873
|
+
|
|
2874
|
+
//#endregion
|
|
2875
|
+
//#region src/OpenApiTransform.ts
|
|
2876
|
+
var OpenApiTransformer = class {
|
|
2877
|
+
createDocument(operations, title = "Extracted API", version = "0.0.0") {
|
|
2878
|
+
const paths = {};
|
|
2879
|
+
for (const operation of operations) {
|
|
2880
|
+
const normalized = this.transformOperation(operation);
|
|
2881
|
+
if (!normalized || this.shouldSkipNormalizedOperation(normalized)) continue;
|
|
2882
|
+
paths[normalized.path] ??= {};
|
|
2883
|
+
paths[normalized.path][normalized.method] = normalized.operation;
|
|
2884
|
+
}
|
|
2885
|
+
return {
|
|
2886
|
+
openapi: "3.1.0",
|
|
2887
|
+
info: {
|
|
2888
|
+
title,
|
|
2889
|
+
version
|
|
2890
|
+
},
|
|
2891
|
+
paths
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
transformOperation(operation) {
|
|
2895
|
+
if (!operation.method || !operation.url) return null;
|
|
2896
|
+
const url = new URL(operation.url);
|
|
2897
|
+
if (this.shouldSkipPlaceholderOperation(url, operation)) return null;
|
|
2898
|
+
const method = operation.method.toLowerCase();
|
|
2899
|
+
const path = this.decodeOpenApiPathname(url.pathname);
|
|
2900
|
+
return {
|
|
2901
|
+
path,
|
|
2902
|
+
method,
|
|
2903
|
+
operation: {
|
|
2904
|
+
summary: operation.sidebarLinks.find((link) => link.active)?.label,
|
|
2905
|
+
description: operation.description ?? void 0,
|
|
2906
|
+
operationId: this.buildOperationId(method, path),
|
|
2907
|
+
parameters: this.createParameters(operation.requestParams),
|
|
2908
|
+
requestBody: this.createRequestBody(operation.requestParams, operation.requestExampleNormalized?.body, this.hasExtractedBodyParams(operation.requestParams) ? null : this.resolveFallbackRequestBodyExample(operation)),
|
|
2909
|
+
responses: this.createResponses(operation.responseSchemas, operation.responseBodies)
|
|
2910
|
+
}
|
|
2911
|
+
};
|
|
2912
|
+
}
|
|
2913
|
+
shouldSkipNormalizedOperation(normalized) {
|
|
2914
|
+
return normalized.path === "/" && normalized.method === "get" && normalized.operation.operationId === "get" && Object.keys(normalized.operation.responses).length === 0;
|
|
2915
|
+
}
|
|
2916
|
+
shouldSkipPlaceholderOperation(url, operation) {
|
|
2917
|
+
if (url.hostname !== "example.com" || url.pathname !== "/") return false;
|
|
2918
|
+
return operation.requestParams.length === 0 && operation.responseSchemas.length === 0 && operation.responseBodies.length === 0 && operation.requestExampleNormalized?.url === "https://example.com/";
|
|
2919
|
+
}
|
|
2920
|
+
decodeOpenApiPathname(pathname) {
|
|
2921
|
+
return pathname.split("/").map((segment) => {
|
|
2922
|
+
if (!segment) return segment;
|
|
2923
|
+
try {
|
|
2924
|
+
return decodeURIComponent(segment);
|
|
2925
|
+
} catch {
|
|
2926
|
+
return segment;
|
|
2927
|
+
}
|
|
2928
|
+
}).join("/");
|
|
2929
|
+
}
|
|
2930
|
+
hasExtractedBodyParams(params) {
|
|
2931
|
+
return params.some((param) => param.in === "body" || param.in === null);
|
|
2932
|
+
}
|
|
2933
|
+
createParameters(params) {
|
|
2934
|
+
const parameters = params.filter((param) => this.isOpenApiParameterLocation(param.in)).map((param) => this.createParameter(param));
|
|
2935
|
+
return parameters.length > 0 ? parameters : void 0;
|
|
2936
|
+
}
|
|
2937
|
+
createRequestBody(params, example, fallbackExample) {
|
|
2938
|
+
const bodyParams = params.filter((param) => param.in === "body" || param.in === null);
|
|
2939
|
+
if (bodyParams.length === 0 && example == null) return;
|
|
2940
|
+
const schema = this.buildRequestBodySchema(bodyParams, example, fallbackExample);
|
|
2941
|
+
return {
|
|
2942
|
+
required: bodyParams.length > 0 ? bodyParams.some((param) => param.required) : false,
|
|
2943
|
+
content: { "application/json": {
|
|
2944
|
+
schema,
|
|
2945
|
+
...example != null ? { example } : {}
|
|
2946
|
+
} }
|
|
2947
|
+
};
|
|
2948
|
+
}
|
|
2949
|
+
buildRequestBodySchema(params, example, fallbackExample) {
|
|
2950
|
+
const schema = this.mergeOpenApiSchemas(this.createExampleSchema(example), this.createExampleSchema(fallbackExample)) ?? { type: "object" };
|
|
2951
|
+
if (example != null) schema.example = example;
|
|
2952
|
+
else if (fallbackExample != null) schema.example = fallbackExample;
|
|
2953
|
+
for (const param of params) this.insertRequestBodyParam(schema, param);
|
|
2954
|
+
return schema;
|
|
2955
|
+
}
|
|
2956
|
+
inferSchemaFromExample(value) {
|
|
2957
|
+
if (Array.isArray(value)) return {
|
|
2958
|
+
type: "array",
|
|
2959
|
+
items: this.inferSchemaFromExample(value[0]) ?? {},
|
|
2960
|
+
example: value
|
|
2961
|
+
};
|
|
2962
|
+
if (isRecord(value)) return {
|
|
2963
|
+
type: "object",
|
|
2964
|
+
properties: Object.fromEntries(Object.entries(value).map(([key, entryValue]) => [key, this.inferSchemaFromExample(entryValue) ?? {}])),
|
|
2965
|
+
example: value
|
|
2966
|
+
};
|
|
2967
|
+
if (typeof value === "string") return {
|
|
2968
|
+
type: "string",
|
|
2969
|
+
example: value
|
|
2970
|
+
};
|
|
2971
|
+
if (typeof value === "number") return {
|
|
2972
|
+
type: Number.isInteger(value) ? "integer" : "number",
|
|
2973
|
+
example: value
|
|
2974
|
+
};
|
|
2975
|
+
if (typeof value === "boolean") return {
|
|
2976
|
+
type: "boolean",
|
|
2977
|
+
example: value
|
|
2978
|
+
};
|
|
2979
|
+
if (value === null) return {};
|
|
2980
|
+
}
|
|
2981
|
+
insertRequestBodyParam(rootSchema, param) {
|
|
2982
|
+
const path = param.path.length > 0 ? param.path : [param.name];
|
|
2983
|
+
let currentSchema = rootSchema;
|
|
2984
|
+
for (const [index, segment] of path.slice(0, -1).entries()) {
|
|
2985
|
+
currentSchema.properties ??= {};
|
|
2986
|
+
currentSchema.properties[segment] ??= { type: "object" };
|
|
2987
|
+
if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], segment]));
|
|
2988
|
+
currentSchema = currentSchema.properties[segment];
|
|
2989
|
+
currentSchema.type ??= "object";
|
|
2990
|
+
if (index === path.length - 2 && param.required) currentSchema.required ??= [];
|
|
2991
|
+
}
|
|
2992
|
+
const leafKey = path[path.length - 1] ?? param.name;
|
|
2993
|
+
currentSchema.properties ??= {};
|
|
2994
|
+
currentSchema.properties[leafKey] = this.createParameterSchema(param);
|
|
2995
|
+
if (param.required) currentSchema.required = Array.from(new Set([...currentSchema.required ?? [], leafKey]));
|
|
2996
|
+
}
|
|
2997
|
+
createParameter(param) {
|
|
2998
|
+
return {
|
|
2999
|
+
name: param.name,
|
|
3000
|
+
in: param.in,
|
|
3001
|
+
required: param.in === "path" ? true : param.required,
|
|
3002
|
+
description: param.description ?? void 0,
|
|
3003
|
+
schema: this.createParameterSchema(param),
|
|
3004
|
+
example: param.defaultValue ?? void 0
|
|
3005
|
+
};
|
|
3006
|
+
}
|
|
3007
|
+
createParameterSchema(param) {
|
|
3008
|
+
return {
|
|
3009
|
+
type: param.type ?? void 0,
|
|
3010
|
+
description: param.description ?? void 0,
|
|
3011
|
+
default: param.defaultValue ?? void 0
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
createResponses(schemas, responseBodies) {
|
|
3015
|
+
const responses = {};
|
|
3016
|
+
for (const schema of schemas) {
|
|
3017
|
+
if (!schema.statusCode) continue;
|
|
3018
|
+
const matchingBodies = responseBodies.filter((body) => body.statusCode === schema.statusCode);
|
|
3019
|
+
const content = this.createResponseContent(matchingBodies);
|
|
3020
|
+
responses[schema.statusCode] = {
|
|
3021
|
+
description: schema.description ?? schema.statusCode,
|
|
3022
|
+
...content ? { content } : {}
|
|
3023
|
+
};
|
|
3024
|
+
}
|
|
3025
|
+
for (const body of responseBodies) {
|
|
3026
|
+
if (!body.statusCode || responses[body.statusCode]) continue;
|
|
3027
|
+
const content = this.createResponseContent([body]);
|
|
3028
|
+
responses[body.statusCode] = {
|
|
3029
|
+
description: body.label ?? body.statusCode,
|
|
3030
|
+
...content ? { content } : {}
|
|
3031
|
+
};
|
|
3032
|
+
}
|
|
3033
|
+
return responses;
|
|
3034
|
+
}
|
|
3035
|
+
createResponseContent(bodies) {
|
|
3036
|
+
if (bodies.length === 0) return;
|
|
3037
|
+
const content = {};
|
|
3038
|
+
for (const body of bodies) {
|
|
3039
|
+
const contentType = body.contentType ?? (body.format === "json" ? "application/json" : "text/plain");
|
|
3040
|
+
const normalizedExample = this.normalizeResponseExampleValue(body.body, body.format);
|
|
3041
|
+
content[contentType] = {
|
|
3042
|
+
schema: this.inferSchemaFromBody(normalizedExample, body.format),
|
|
3043
|
+
example: normalizedExample
|
|
3044
|
+
};
|
|
3045
|
+
}
|
|
3046
|
+
return content;
|
|
3047
|
+
}
|
|
3048
|
+
inferSchemaFromBody(body, format) {
|
|
3049
|
+
if (format === "json") return this.inferSchemaFromExample(body);
|
|
3050
|
+
if (format === "text") return {
|
|
3051
|
+
type: "string",
|
|
3052
|
+
example: body
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
normalizeResponseExampleValue(body, format) {
|
|
3056
|
+
if (format !== "json" || typeof body !== "string") return body;
|
|
3057
|
+
return JsonRepair.parsePossiblyTruncated(body) ?? parseLooseStructuredValue(body) ?? body;
|
|
3058
|
+
}
|
|
3059
|
+
resolveFallbackRequestBodyExample(operation) {
|
|
3060
|
+
const jsonResponseBody = operation.responseBodies.find((body) => body.format === "json")?.body;
|
|
3061
|
+
if (jsonResponseBody != null) return jsonResponseBody;
|
|
3062
|
+
if (typeof operation.responseExample === "object" && operation.responseExample !== null) return operation.responseExample;
|
|
3063
|
+
if (typeof operation.responseExampleRaw === "string") return JsonRepair.parsePossiblyTruncated(operation.responseExampleRaw);
|
|
3064
|
+
if (typeof operation.responseExample === "string") return JsonRepair.parsePossiblyTruncated(operation.responseExample);
|
|
3065
|
+
return null;
|
|
3066
|
+
}
|
|
3067
|
+
createExampleSchema(value) {
|
|
3068
|
+
if (value == null) return null;
|
|
3069
|
+
return this.inferSchemaFromExample(value) ?? null;
|
|
3070
|
+
}
|
|
3071
|
+
mergeOpenApiSchemas(left, right) {
|
|
3072
|
+
if (!left) return right;
|
|
3073
|
+
if (!right) return left;
|
|
3074
|
+
const merged = {
|
|
3075
|
+
...right,
|
|
3076
|
+
...left,
|
|
3077
|
+
...left.type || right.type ? { type: left.type ?? right.type } : {},
|
|
3078
|
+
...left.description || right.description ? { description: left.description ?? right.description } : {},
|
|
3079
|
+
...left.default !== void 0 || right.default !== void 0 ? { default: left.default ?? right.default } : {},
|
|
3080
|
+
...left.example !== void 0 || right.example !== void 0 ? { example: left.example ?? right.example } : {}
|
|
3081
|
+
};
|
|
3082
|
+
if (left.properties || right.properties) {
|
|
3083
|
+
const propertyKeys = new Set([...Object.keys(left.properties ?? {}), ...Object.keys(right.properties ?? {})]);
|
|
3084
|
+
merged.properties = Object.fromEntries(Array.from(propertyKeys).map((key) => [key, this.mergeOpenApiSchemas(left.properties?.[key] ?? null, right.properties?.[key] ?? null) ?? {}]));
|
|
3085
|
+
}
|
|
3086
|
+
if (left.items || right.items) merged.items = this.mergeOpenApiSchemas(left.items ?? null, right.items ?? null) ?? {};
|
|
3087
|
+
if (left.required || right.required) merged.required = Array.from(new Set([...right.required ?? [], ...left.required ?? []]));
|
|
3088
|
+
return merged;
|
|
3089
|
+
}
|
|
3090
|
+
buildOperationId(method, path) {
|
|
3091
|
+
return `${method}${path.replace(/\{([^}]+)\}/g, "$1").split("/").filter(Boolean).map((segment) => segment.replace(/[^a-zA-Z0-9]+/g, " ")).map((segment) => segment.trim()).filter(Boolean).map((segment) => segment.charAt(0).toUpperCase() + segment.slice(1).replace(/\s+(.)/g, (_match, char) => char.toUpperCase())).join("")}`;
|
|
3092
|
+
}
|
|
3093
|
+
isOpenApiParameterLocation(value) {
|
|
3094
|
+
return value === "query" || value === "header" || value === "path" || value === "cookie";
|
|
3095
|
+
}
|
|
3096
|
+
};
|
|
3097
|
+
const transformer = new OpenApiTransformer();
|
|
3098
|
+
|
|
3099
|
+
//#endregion
|
|
3100
|
+
//#region src/Commands/GenerateCommand.ts
|
|
3101
|
+
var GenerateCommand = class extends _h3ravel_musket.Command {
|
|
3102
|
+
signature = `generate
|
|
3103
|
+
{artifact : Artifact to generate [sdk]}
|
|
3104
|
+
{source? : Documentation URL/local source or parsed TypeScript output file}
|
|
3105
|
+
{--d|dir? : Output directory for the generated artifact}
|
|
3106
|
+
{--n|name? : Package name for generated SDK packages}
|
|
3107
|
+
{--O|output-mode=both : SDK output mode [runtime,classes,both]}
|
|
3108
|
+
{--S|signature-style=grouped : SDK method signature style [flat,grouped]}
|
|
3109
|
+
{--N|namespace-strategy=smart : Namespace naming strategy [smart,scoped]}
|
|
3110
|
+
{--M|method-strategy=smart : Method naming strategy [smart,operation-id]}
|
|
3111
|
+
{--r|root-type-name=ExtractedApiDocument : Root type name for the generated Schema.ts module}
|
|
3112
|
+
{--B|browser? : Remote loader [axios,happy-dom,jsdom,puppeteer]}
|
|
3113
|
+
{--t|timeout? : Request/browser timeout in milliseconds}
|
|
3114
|
+
{--c|crawl : Crawl sidebar links and parse every discovered operation}
|
|
3115
|
+
{--b|base-url? : Base URL used to resolve sidebar links when crawling from a local file}
|
|
3116
|
+
`;
|
|
3117
|
+
description = "Generate artifacts such as SDK packages from documentation sources or parsed TypeScript outputs";
|
|
3118
|
+
async handle() {
|
|
3119
|
+
const conf = this.app.getConfig();
|
|
3120
|
+
const artifact = String(this.argument("artifact", "")).trim().toLowerCase();
|
|
3121
|
+
const source = String(this.argument("source", "")).trim();
|
|
3122
|
+
const browser = String(this.option("browser", conf.browser)).trim().toLowerCase();
|
|
3123
|
+
const timeoutOption = String(this.option("timeout", "")).trim();
|
|
3124
|
+
const crawl = this.option("crawl");
|
|
3125
|
+
const baseUrl = String(this.option("baseUrl", "")).trim() || null;
|
|
3126
|
+
const packageDir = await this.resolveOutputDirectory(source);
|
|
3127
|
+
const spinner = this.spinner(`Generating ${artifact} artifact...`).start();
|
|
3128
|
+
let startedBrowserSession = false;
|
|
3129
|
+
try {
|
|
3130
|
+
const start = Date.now();
|
|
3131
|
+
if (!isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
|
|
3132
|
+
if (artifact !== "sdk") throw new Error(`Unsupported artifact: ${artifact}`);
|
|
3133
|
+
if (!source) throw new Error("The sdk artifact requires a source argument");
|
|
3134
|
+
if (!this.isTypeScriptArtifactSource(source) && !isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
|
|
3135
|
+
const requestTimeout = this.resolveTimeoutOverride(timeoutOption, conf.requestTimeout);
|
|
3136
|
+
const namespaceStrategy = this.parseNamespaceStrategy(this.option("namespaceStrategy", "smart"));
|
|
3137
|
+
const methodStrategy = this.parseMethodStrategy(this.option("methodStrategy", "smart"));
|
|
3138
|
+
const outputMode = this.parseOutputMode(this.option("outputMode", "both"));
|
|
3139
|
+
const signatureStyle = this.parseSignatureStyle(this.option("signatureStyle", "grouped"));
|
|
3140
|
+
const rootTypeName = String(this.option("rootTypeName", "ExtractedApiDocument")).trim() || "ExtractedApiDocument";
|
|
3141
|
+
this.app.configure({
|
|
3142
|
+
browser,
|
|
3143
|
+
requestTimeout
|
|
3144
|
+
});
|
|
3145
|
+
if (!this.isTypeScriptArtifactSource(source) && crawl) {
|
|
3146
|
+
await startBrowserSession(this.app.getConfig());
|
|
3147
|
+
startedBrowserSession = true;
|
|
3148
|
+
}
|
|
3149
|
+
const sdkSource = await this.resolveSdkSource({
|
|
3150
|
+
source,
|
|
3151
|
+
crawl,
|
|
3152
|
+
baseUrl,
|
|
3153
|
+
rootTypeName,
|
|
3154
|
+
namespaceStrategy,
|
|
3155
|
+
methodStrategy
|
|
3156
|
+
});
|
|
3157
|
+
const packageName = this.resolvePackageName(packageDir);
|
|
3158
|
+
const files = new SdkPackageGenerator().generate(sdkSource.document, {
|
|
3159
|
+
outputMode,
|
|
3160
|
+
signatureStyle,
|
|
3161
|
+
rootTypeName,
|
|
3162
|
+
namespaceStrategy,
|
|
3163
|
+
methodStrategy,
|
|
3164
|
+
schemaModule: sdkSource.schemaModule,
|
|
3165
|
+
packageName: String(this.option("name", "")).trim() || packageName
|
|
3166
|
+
});
|
|
3167
|
+
await this.writePackageFiles(packageDir, files);
|
|
3168
|
+
const duration = Date.now() - start;
|
|
3169
|
+
_h3ravel_shared.Logger.twoColumnDetail(_h3ravel_shared.Logger.log([["Generated", "green"], [`${duration / 1e3}s`, "gray"]], " ", false), packageDir.replace(process.cwd(), "."));
|
|
3170
|
+
spinner.succeed("Artifact generation completed");
|
|
3171
|
+
} catch (error) {
|
|
3172
|
+
const message = error instanceof Error ? error.message : "Unknown error";
|
|
3173
|
+
spinner.fail(`Failed to generate artifact: ${message}`);
|
|
3174
|
+
process.exitCode = 1;
|
|
3175
|
+
} finally {
|
|
3176
|
+
if (startedBrowserSession) await endBrowserSession();
|
|
3177
|
+
}
|
|
3178
|
+
}
|
|
3179
|
+
/**
|
|
3180
|
+
* Resolves the SDK source by either loading a pre-generated TypeScript artifact or
|
|
3181
|
+
* crawling/parsing HTML documentation based on the provided options and source string.
|
|
3182
|
+
*
|
|
3183
|
+
* @param options The options for resolving the SDK source.
|
|
3184
|
+
* @returns An object containing the OpenAPI document and the generated schema module as a string.
|
|
3185
|
+
*/
|
|
3186
|
+
async resolveSdkSource(options) {
|
|
3187
|
+
if (this.isTypeScriptArtifactSource(options.source)) return this.loadSdkSourceFromTypeScriptArtifact(options.source);
|
|
3188
|
+
const operation = extractReadmeOperationFromHtml(await this.app.loadHtmlSource(options.source, true));
|
|
3189
|
+
const payload = options.crawl ? await this.app.crawlReadmeOperations(options.source, operation, options.baseUrl) : operation;
|
|
3190
|
+
const document = this.buildOpenApiPayload(payload);
|
|
3191
|
+
return {
|
|
3192
|
+
document,
|
|
3193
|
+
schemaModule: await prettier.default.format(TypeScriptGenerator.generateModule(document, options.rootTypeName, {
|
|
3194
|
+
namespaceStrategy: options.namespaceStrategy,
|
|
3195
|
+
methodStrategy: options.methodStrategy
|
|
3196
|
+
}), {
|
|
3197
|
+
parser: "typescript",
|
|
3198
|
+
semi: false,
|
|
3199
|
+
singleQuote: true
|
|
3200
|
+
})
|
|
3201
|
+
};
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Loads the SDK source from a pre-generated TypeScript artifact.
|
|
3205
|
+
*
|
|
3206
|
+
* @param source
|
|
3207
|
+
* @returns
|
|
3208
|
+
*/
|
|
3209
|
+
async loadSdkSourceFromTypeScriptArtifact(source) {
|
|
3210
|
+
const filePath = node_path.default.resolve(process.cwd(), source);
|
|
3211
|
+
const schemaModule = await node_fs_promises.default.readFile(filePath, "utf8");
|
|
3212
|
+
const importedModule = await import(`${(0, node_url.pathToFileURL)(filePath).href}?t=${Date.now()}`);
|
|
3213
|
+
const documentCandidate = importedModule.default ?? Object.values(importedModule).find((value) => this.isOpenApiDocumentLike(value));
|
|
3214
|
+
if (!this.isOpenApiDocumentLike(documentCandidate)) throw new Error("The provided TypeScript source does not export an OpenAPI document");
|
|
3215
|
+
return {
|
|
3216
|
+
document: documentCandidate,
|
|
3217
|
+
schemaModule
|
|
3218
|
+
};
|
|
3219
|
+
}
|
|
3220
|
+
/**
|
|
3221
|
+
* Builds an OpenAPI document from the extracted operations.
|
|
3222
|
+
*
|
|
3223
|
+
* @param payload
|
|
3224
|
+
* @returns
|
|
3225
|
+
*/
|
|
3226
|
+
buildOpenApiPayload(payload) {
|
|
3227
|
+
if ("operations" in payload) return transformer.createDocument(payload.operations, "Extracted API", "0.0.0");
|
|
3228
|
+
return transformer.createDocument([payload], "Extracted API", "0.0.0");
|
|
3229
|
+
}
|
|
3230
|
+
resolveTimeoutOverride(value, fallback) {
|
|
3231
|
+
if (!value) return fallback;
|
|
3232
|
+
const parsed = Number(value);
|
|
3233
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid timeout override: ${value}`);
|
|
3234
|
+
return parsed;
|
|
3235
|
+
}
|
|
3236
|
+
/**
|
|
3237
|
+
* Resolves the output directory for the generated SDK package.
|
|
3238
|
+
*
|
|
3239
|
+
* @param source The source string used to determine the output directory.
|
|
3240
|
+
* @returns The resolved output directory path.
|
|
3241
|
+
*/
|
|
3242
|
+
async resolveOutputDirectory(source, explicitDir) {
|
|
3243
|
+
explicitDir ??= String(this.option("dir", "")).trim();
|
|
3244
|
+
const dir = explicitDir ? node_path.default.resolve(process.cwd(), explicitDir) : OutputGenerator.buildArtifactDirectory(process.cwd(), source, "sdk");
|
|
3245
|
+
if ((0, node_fs.existsSync)(dir)) {
|
|
3246
|
+
if ((0, node_fs.readdirSync)(dir).length > 0) switch (await this.choice(`Output directory (${explicitDir}) already exists and is not empty, what would you like to do?`, [
|
|
3247
|
+
{
|
|
3248
|
+
name: "Overwrite",
|
|
3249
|
+
value: "overwrite"
|
|
3250
|
+
},
|
|
3251
|
+
{
|
|
3252
|
+
name: "Try to merge",
|
|
3253
|
+
value: "merge"
|
|
3254
|
+
},
|
|
3255
|
+
{
|
|
3256
|
+
name: "Choose a different directory",
|
|
3257
|
+
value: "choose"
|
|
3258
|
+
},
|
|
3259
|
+
{
|
|
3260
|
+
name: "Cancel",
|
|
3261
|
+
value: "cancel"
|
|
3262
|
+
}
|
|
3263
|
+
])) {
|
|
3264
|
+
case "overwrite":
|
|
3265
|
+
await node_fs_promises.default.rm(dir, {
|
|
3266
|
+
recursive: true,
|
|
3267
|
+
force: true
|
|
3268
|
+
});
|
|
3269
|
+
break;
|
|
3270
|
+
case "choose": {
|
|
3271
|
+
const newDir = await this.ask("Please enter a new output directory (relative to current directory):", explicitDir);
|
|
3272
|
+
return this.resolveOutputDirectory(source, newDir);
|
|
3273
|
+
}
|
|
3274
|
+
case "cancel":
|
|
3275
|
+
this.info("Operation cancelled by user");
|
|
3276
|
+
return process.exit(0);
|
|
3277
|
+
default: break;
|
|
3278
|
+
}
|
|
3279
|
+
}
|
|
3280
|
+
return dir;
|
|
3281
|
+
}
|
|
3282
|
+
resolvePackageName(packageDir) {
|
|
3283
|
+
const explicitName = String(this.option("name", "")).trim();
|
|
3284
|
+
if (explicitName) return explicitName;
|
|
3285
|
+
return node_path.default.basename(packageDir).replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "generated-sdk";
|
|
3286
|
+
}
|
|
3287
|
+
parseOutputMode(value) {
|
|
3288
|
+
const normalized = String(value ?? "both").trim().toLowerCase();
|
|
3289
|
+
if (normalized === "runtime" || normalized === "classes" || normalized === "both") return normalized;
|
|
3290
|
+
throw new Error(`Unsupported sdk output mode: ${normalized}`);
|
|
3291
|
+
}
|
|
3292
|
+
parseSignatureStyle(value) {
|
|
3293
|
+
const normalized = String(value ?? "grouped").trim().toLowerCase();
|
|
3294
|
+
if (normalized === "flat" || normalized === "grouped") return normalized;
|
|
3295
|
+
throw new Error(`Unsupported signature style: ${normalized}`);
|
|
3296
|
+
}
|
|
3297
|
+
parseNamespaceStrategy(value) {
|
|
3298
|
+
const normalized = String(value ?? "smart").trim().toLowerCase();
|
|
3299
|
+
if (normalized === "smart" || normalized === "scoped") return normalized;
|
|
3300
|
+
throw new Error(`Unsupported namespace strategy: ${normalized}`);
|
|
3301
|
+
}
|
|
3302
|
+
parseMethodStrategy(value) {
|
|
3303
|
+
const normalized = String(value ?? "smart").trim().toLowerCase();
|
|
3304
|
+
if (normalized === "smart" || normalized === "operation-id") return normalized;
|
|
3305
|
+
throw new Error(`Unsupported method strategy: ${normalized}`);
|
|
3306
|
+
}
|
|
3307
|
+
/**
|
|
3308
|
+
* Checks if the provided source string points to a TypeScript or JavaScript file, which is
|
|
3309
|
+
* used to determine if the source should be loaded as a pre-generated artifact instead
|
|
3310
|
+
* of crawling/parsing HTML documentation.
|
|
3311
|
+
*
|
|
3312
|
+
* @param source The source string to check.
|
|
3313
|
+
* @returns True if the source is a TypeScript or JavaScript file, false otherwise.
|
|
3314
|
+
*/
|
|
3315
|
+
isTypeScriptArtifactSource(source) {
|
|
3316
|
+
return /\.(?:[cm]?ts|[cm]?js)$/i.test(source);
|
|
3317
|
+
}
|
|
3318
|
+
/**
|
|
3319
|
+
* Type guard to check if a value conforms to the OpenApiDocumentLike interface.
|
|
3320
|
+
*
|
|
3321
|
+
* @param value
|
|
3322
|
+
* @returns
|
|
3323
|
+
*/
|
|
3324
|
+
isOpenApiDocumentLike(value) {
|
|
3325
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
3326
|
+
const candidate = value;
|
|
3327
|
+
const info = candidate.info;
|
|
3328
|
+
return candidate.openapi === "3.1.0" && typeof info === "object" && info !== null && !Array.isArray(info) && typeof info.title === "string" && typeof info.version === "string" && typeof candidate.paths === "object" && candidate.paths !== null && !Array.isArray(candidate.paths);
|
|
3329
|
+
}
|
|
3330
|
+
/**
|
|
3331
|
+
* Writes the generated files to the output directory, creating any necessary subdirectories.
|
|
3332
|
+
*
|
|
3333
|
+
* @param packageDir
|
|
3334
|
+
* @param files
|
|
3335
|
+
*/
|
|
3336
|
+
async writePackageFiles(packageDir, files) {
|
|
3337
|
+
await Promise.all(Object.entries(files).map(async ([relativePath, content]) => {
|
|
3338
|
+
const filePath = node_path.default.join(packageDir, relativePath);
|
|
3339
|
+
await node_fs_promises.default.mkdir(node_path.default.dirname(filePath), { recursive: true });
|
|
3340
|
+
await node_fs_promises.default.writeFile(filePath, content, "utf8");
|
|
3341
|
+
}));
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
|
|
3345
|
+
//#endregion
|
|
3346
|
+
//#region src/Commands/InitCommand.ts
|
|
3347
|
+
const __filename$1 = (0, url.fileURLToPath)(require("url").pathToFileURL(__filename).href);
|
|
3348
|
+
var InitCommand = class extends _h3ravel_musket.Command {
|
|
3349
|
+
signature = `init
|
|
3350
|
+
{--f|force : Overwrite existing config}
|
|
3351
|
+
{--p|pkg? : Generate config for another package (e.g. sdk-kit) instead of oapiex [sdk]}
|
|
3352
|
+
`;
|
|
3353
|
+
description = "Generate a default oapiex.config.ts in the current directory";
|
|
3354
|
+
async handle() {
|
|
3355
|
+
const cwd = process.cwd();
|
|
3356
|
+
const configPath = node_path.default.join(cwd, "oapiex.config.js");
|
|
3357
|
+
const force = this.option("force", false);
|
|
3358
|
+
const pkg = this.option("pkg", "base").trim().toLowerCase();
|
|
3359
|
+
const configTemplate = {
|
|
3360
|
+
base: this.buildConfigTemplate(),
|
|
3361
|
+
sdk: this.buildSdkConfigTemplate()
|
|
3362
|
+
};
|
|
3363
|
+
if (!["base", "sdk"].includes(pkg)) return void this.error(`Invalid package option: ${pkg}`);
|
|
3364
|
+
try {
|
|
3365
|
+
await node_fs_promises.default.access(configPath);
|
|
3366
|
+
if (!force) {
|
|
3367
|
+
this.error(`Config file already exists at ${configPath}. Use --force to overwrite.`);
|
|
3368
|
+
process.exit(1);
|
|
3369
|
+
}
|
|
3370
|
+
} catch {}
|
|
3371
|
+
await node_fs_promises.default.writeFile(configPath, configTemplate[pkg], "utf8");
|
|
3372
|
+
this.line(`Created ${configPath} `);
|
|
3373
|
+
}
|
|
3374
|
+
buildConfigTemplate() {
|
|
3375
|
+
const def = defaultConfig;
|
|
3376
|
+
return [
|
|
3377
|
+
`import { defineConfig } from '${__filename$1.includes("node_modules") ? "oapiex" : "./src/Manager"}'`,
|
|
3378
|
+
"",
|
|
3379
|
+
"/**",
|
|
3380
|
+
" * See https://toneflix.github.io/oapiex/configuration for docs",
|
|
3381
|
+
" */",
|
|
3382
|
+
"export default defineConfig({",
|
|
3383
|
+
` outputFormat: '${def.outputFormat}',`,
|
|
3384
|
+
` outputShape: '${def.outputShape}',`,
|
|
3385
|
+
` browser: '${def.browser}',`,
|
|
3386
|
+
` requestTimeout: ${def.requestTimeout},`,
|
|
3387
|
+
` maxRedirects: ${def.maxRedirects},`,
|
|
3388
|
+
` userAgent: '${def.userAgent}',`,
|
|
3389
|
+
` retryCount: ${def.retryCount},`,
|
|
3390
|
+
` retryDelay: ${def.retryDelay},`,
|
|
3391
|
+
"})"
|
|
3392
|
+
].join("\n");
|
|
3393
|
+
}
|
|
3394
|
+
buildSdkConfigTemplate() {
|
|
3395
|
+
return [
|
|
3396
|
+
"import { defineConfig } from '@oapiex/sdk-kit'",
|
|
3397
|
+
"",
|
|
3398
|
+
"/**",
|
|
3399
|
+
" * See https://toneflix.github.io/oapiex/configuration for docs",
|
|
3400
|
+
" */",
|
|
3401
|
+
"export default defineConfig({",
|
|
3402
|
+
" environment: 'sandbox',",
|
|
3403
|
+
" urls: {",
|
|
3404
|
+
" live: 'https://live.oapiex.com',",
|
|
3405
|
+
" sandbox: 'https://sandbox.oapiex.com',",
|
|
3406
|
+
" },",
|
|
3407
|
+
"})"
|
|
3408
|
+
].join("\n");
|
|
3409
|
+
}
|
|
1226
3410
|
};
|
|
1227
3411
|
|
|
1228
3412
|
//#endregion
|
|
@@ -1230,9 +3414,10 @@ const isOpenApiParameterLocation = (value) => {
|
|
|
1230
3414
|
var ParseCommand = class extends _h3ravel_musket.Command {
|
|
1231
3415
|
signature = `parse
|
|
1232
3416
|
{source : Local HTML file path or remote URL}
|
|
1233
|
-
{--O|output=pretty : Output format [pretty,json,js]}
|
|
3417
|
+
{--O|output=pretty : Output format [pretty,json,js,ts]}
|
|
1234
3418
|
{--S|shape=raw : Result shape [raw,openapi]}
|
|
1235
3419
|
{--B|browser? : Remote loader [axios,happy-dom,jsdom,puppeteer]}
|
|
3420
|
+
{--t|timeout? : Request/browser timeout in milliseconds}
|
|
1236
3421
|
{--c|crawl : Crawl sidebar links and parse every discovered operation}
|
|
1237
3422
|
{--b|base-url? : Base URL used to resolve sidebar links when crawling from a local file}
|
|
1238
3423
|
`;
|
|
@@ -1243,6 +3428,7 @@ var ParseCommand = class extends _h3ravel_musket.Command {
|
|
|
1243
3428
|
const output = String(this.option("output", conf.outputFormat)).trim().toLowerCase();
|
|
1244
3429
|
const shape = String(this.option("shape", conf.outputShape)).trim().toLowerCase();
|
|
1245
3430
|
const browser = String(this.option("browser", conf.browser)).trim().toLowerCase();
|
|
3431
|
+
const timeoutOption = String(this.option("timeout", "")).trim();
|
|
1246
3432
|
const crawl = this.option("crawl");
|
|
1247
3433
|
const baseUrl = String(this.option("baseUrl", "")).trim() || null;
|
|
1248
3434
|
const spinner = this.spinner(`${crawl ? "Crawling and p" : "P"}arsing source...`).start();
|
|
@@ -1250,7 +3436,11 @@ var ParseCommand = class extends _h3ravel_musket.Command {
|
|
|
1250
3436
|
try {
|
|
1251
3437
|
const start = Date.now();
|
|
1252
3438
|
if (!isSupportedBrowser(browser)) throw new Error(`Unsupported browser: ${browser}`);
|
|
1253
|
-
this.
|
|
3439
|
+
const requestTimeout = this.resolveTimeoutOverride(timeoutOption, conf.requestTimeout);
|
|
3440
|
+
this.app.configure({
|
|
3441
|
+
browser,
|
|
3442
|
+
requestTimeout
|
|
3443
|
+
});
|
|
1254
3444
|
if (crawl) {
|
|
1255
3445
|
await startBrowserSession(this.app.getConfig());
|
|
1256
3446
|
startedBrowserSession = true;
|
|
@@ -1258,7 +3448,7 @@ var ParseCommand = class extends _h3ravel_musket.Command {
|
|
|
1258
3448
|
const operation = extractReadmeOperationFromHtml(await this.app.loadHtmlSource(source, true));
|
|
1259
3449
|
const payload = crawl ? await this.app.crawlReadmeOperations(source, operation, baseUrl) : operation;
|
|
1260
3450
|
const normalizedPayload = shape === "openapi" ? this.buildOpenApiPayload(payload) : payload;
|
|
1261
|
-
const serialized =
|
|
3451
|
+
const serialized = await OutputGenerator.serializeOutput(normalizedPayload, output, OutputGenerator.getRootTypeName(shape));
|
|
1262
3452
|
const filePath = await this.saveOutputToFile(serialized, source, shape, output);
|
|
1263
3453
|
const duration = Date.now() - start;
|
|
1264
3454
|
_h3ravel_shared.Logger.twoColumnDetail(_h3ravel_shared.Logger.log([["Output", "green"], [`${duration / 1e3}s`, "gray"]], " ", false), filePath.replace(process.cwd(), "."));
|
|
@@ -1271,27 +3461,22 @@ var ParseCommand = class extends _h3ravel_musket.Command {
|
|
|
1271
3461
|
if (startedBrowserSession) await endBrowserSession();
|
|
1272
3462
|
}
|
|
1273
3463
|
}
|
|
3464
|
+
resolveTimeoutOverride = (value, fallback) => {
|
|
3465
|
+
if (!value) return fallback;
|
|
3466
|
+
const parsed = Number(value);
|
|
3467
|
+
if (!Number.isFinite(parsed) || parsed <= 0) throw new Error(`Invalid timeout override: ${value}`);
|
|
3468
|
+
return parsed;
|
|
3469
|
+
};
|
|
1274
3470
|
buildOpenApiPayload = (payload) => {
|
|
1275
|
-
if ("operations" in payload) return
|
|
1276
|
-
return
|
|
3471
|
+
if ("operations" in payload) return transformer.createDocument(payload.operations, "Extracted API", "0.0.0");
|
|
3472
|
+
return transformer.createDocument([payload], "Extracted API", "0.0.0");
|
|
1277
3473
|
};
|
|
1278
3474
|
saveOutputToFile = async (content, source, shape, outputFormat) => {
|
|
1279
|
-
const
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
const outputDir = node_path.default.resolve(process.cwd(), "output");
|
|
1285
|
-
await node_fs_promises.default.mkdir(outputDir, { recursive: true });
|
|
1286
|
-
const filename = `${source.replace(/[^a-zA-Z0-9_-]+/g, "_").replace(/^_+|_+$/g, "") || "output"}${shape === "openapi" ? ".openapi" : ""}.${ext}`;
|
|
1287
|
-
const filePath = node_path.default.join(outputDir, filename);
|
|
1288
|
-
if (outputFormat === "js") content = await prettier.default.format(content, {
|
|
1289
|
-
parser: "babel",
|
|
1290
|
-
semi: false,
|
|
1291
|
-
singleQuote: true
|
|
1292
|
-
});
|
|
1293
|
-
await node_fs_promises.default.writeFile(filePath, content, "utf8");
|
|
1294
|
-
return filePath;
|
|
3475
|
+
const outputDir = OutputGenerator.buildFilePath(process.cwd(), source, shape, outputFormat);
|
|
3476
|
+
const outputDirname = node_path.default.dirname(outputDir);
|
|
3477
|
+
await node_fs_promises.default.mkdir(outputDirname, { recursive: true });
|
|
3478
|
+
await node_fs_promises.default.writeFile(outputDir, content, "utf8");
|
|
3479
|
+
return outputDir;
|
|
1295
3480
|
};
|
|
1296
3481
|
};
|
|
1297
3482
|
|
|
@@ -1332,21 +3517,16 @@ async function resolveConfig(cliOverrides = {}) {
|
|
|
1332
3517
|
|
|
1333
3518
|
//#endregion
|
|
1334
3519
|
exports.Application = Application;
|
|
3520
|
+
exports.GenerateCommand = GenerateCommand;
|
|
1335
3521
|
exports.InitCommand = InitCommand;
|
|
3522
|
+
exports.JsonRepair = JsonRepair;
|
|
3523
|
+
exports.OpenApiTransformer = OpenApiTransformer;
|
|
3524
|
+
exports.OutputGenerator = OutputGenerator;
|
|
1336
3525
|
exports.ParseCommand = ParseCommand;
|
|
3526
|
+
exports.SdkPackageGenerator = SdkPackageGenerator;
|
|
3527
|
+
exports.TypeScriptGenerator = TypeScriptGenerator;
|
|
1337
3528
|
exports.browser = browser;
|
|
1338
|
-
exports.buildOperationId = buildOperationId;
|
|
1339
3529
|
exports.buildOperationUrl = buildOperationUrl;
|
|
1340
|
-
exports.buildRequestBodySchema = buildRequestBodySchema;
|
|
1341
|
-
exports.createExampleSchema = createExampleSchema;
|
|
1342
|
-
exports.createOpenApiDocumentFromReadmeOperations = createOpenApiDocumentFromReadmeOperations;
|
|
1343
|
-
exports.createParameter = createParameter;
|
|
1344
|
-
exports.createParameterSchema = createParameterSchema;
|
|
1345
|
-
exports.createParameters = createParameters;
|
|
1346
|
-
exports.createRequestBody = createRequestBody;
|
|
1347
|
-
exports.createResponseContent = createResponseContent;
|
|
1348
|
-
exports.createResponses = createResponses;
|
|
1349
|
-
exports.decodeOpenApiPathname = decodeOpenApiPathname;
|
|
1350
3530
|
exports.defaultConfig = defaultConfig;
|
|
1351
3531
|
exports.defineConfig = defineConfig;
|
|
1352
3532
|
exports.endBrowserSession = endBrowserSession;
|
|
@@ -1375,6 +3555,7 @@ exports.extractResponseSchemas = extractResponseSchemas;
|
|
|
1375
3555
|
exports.extractResponseSchemasFromOpenApi = extractResponseSchemasFromOpenApi;
|
|
1376
3556
|
exports.extractSidebarLinkLabel = extractSidebarLinkLabel;
|
|
1377
3557
|
exports.extractSidebarLinks = extractSidebarLinks;
|
|
3558
|
+
exports.extractStablePageHtml = extractStablePageHtml;
|
|
1378
3559
|
exports.extractStringLiteralValue = extractStringLiteralValue;
|
|
1379
3560
|
exports.findParameterRoot = findParameterRoot;
|
|
1380
3561
|
exports.flattenOpenApiSchemaProperties = flattenOpenApiSchemaProperties;
|
|
@@ -1385,20 +3566,14 @@ Object.defineProperty(exports, 'globalConfig', {
|
|
|
1385
3566
|
return globalConfig;
|
|
1386
3567
|
}
|
|
1387
3568
|
});
|
|
1388
|
-
exports.hasExtractedBodyParams = hasExtractedBodyParams;
|
|
1389
3569
|
exports.inferParameterLocation = inferParameterLocation;
|
|
1390
3570
|
exports.inferParameterLocationFromText = inferParameterLocationFromText;
|
|
1391
3571
|
exports.inferParameterPath = inferParameterPath;
|
|
1392
3572
|
exports.inferParameterType = inferParameterType;
|
|
1393
|
-
exports.inferSchemaFromBody = inferSchemaFromBody;
|
|
1394
|
-
exports.inferSchemaFromExample = inferSchemaFromExample;
|
|
1395
|
-
exports.insertRequestBodyParam = insertRequestBodyParam;
|
|
1396
|
-
exports.isOpenApiParameterLocation = isOpenApiParameterLocation;
|
|
1397
3573
|
exports.isRecord = isRecord;
|
|
1398
3574
|
exports.isRequiredParameter = isRequiredParameter;
|
|
1399
3575
|
exports.isSupportedBrowser = isSupportedBrowser;
|
|
1400
3576
|
exports.loadUserConfig = loadUserConfig;
|
|
1401
|
-
exports.mergeOpenApiSchemas = mergeOpenApiSchemas;
|
|
1402
3577
|
exports.mergeReadmeOperations = mergeReadmeOperations;
|
|
1403
3578
|
exports.mergeSsrPropsIntoRenderedHtml = mergeSsrPropsIntoRenderedHtml;
|
|
1404
3579
|
exports.normalizeCurlSnippet = normalizeCurlSnippet;
|
|
@@ -1407,18 +3582,14 @@ exports.normalizeRequestCodeSnippet = normalizeRequestCodeSnippet;
|
|
|
1407
3582
|
exports.normalizeResponseBody = normalizeResponseBody;
|
|
1408
3583
|
exports.normalizeStructuredRequestBody = normalizeStructuredRequestBody;
|
|
1409
3584
|
exports.parseLooseStructuredValue = parseLooseStructuredValue;
|
|
1410
|
-
exports.parsePossiblyTruncatedJson = parsePossiblyTruncatedJson;
|
|
1411
3585
|
exports.readInputValue = readInputValue;
|
|
1412
3586
|
exports.readText = readText;
|
|
1413
3587
|
exports.readTexts = readTexts;
|
|
1414
3588
|
exports.resolveConfig = resolveConfig;
|
|
1415
|
-
exports.resolveFallbackRequestBodyExample = resolveFallbackRequestBodyExample;
|
|
1416
3589
|
exports.resolveOpenApiMediaExample = resolveOpenApiMediaExample;
|
|
1417
3590
|
exports.resolveParameterInput = resolveParameterInput;
|
|
1418
3591
|
exports.resolveReadmeSidebarUrls = resolveReadmeSidebarUrls;
|
|
1419
3592
|
exports.resolveSsrOperation = resolveSsrOperation;
|
|
1420
|
-
exports.shouldSkipNormalizedOperation = shouldSkipNormalizedOperation;
|
|
1421
|
-
exports.shouldSkipPlaceholderOperation = shouldSkipPlaceholderOperation;
|
|
1422
3593
|
exports.startBrowserSession = startBrowserSession;
|
|
1423
3594
|
exports.supportedBrowsers = supportedBrowsers;
|
|
1424
|
-
exports.
|
|
3595
|
+
exports.transformer = transformer;
|