apigen-ts 0.1.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_template.ts +17 -4
- package/dist/cli.cjs +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/{main-bbe33f1b.js → main-BTcttgNf.js} +62 -95
- package/dist/{main-bbe33f1b.mjs → main-BTcttgNf.mjs} +62 -95
- package/dist/{main-c2426ec2.cjs → main-oWgfChRG.cjs} +62 -95
- package/dist/main.cjs +1 -1
- package/dist/main.js +1 -1
- package/dist/main.mjs +1 -1
- package/package.json +19 -19
- package/readme.md +50 -2
package/dist/_template.ts
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
// Note: Use uppercase for names in ApiClient to avoid conflict with the generated code
|
|
2
2
|
|
|
3
|
+
type Headers = Record<string, string>
|
|
4
|
+
export type ApigenHeaders = Headers | ((method: string, path: string) => Headers | Promise<Headers>)
|
|
5
|
+
|
|
3
6
|
export interface ApigenConfig {
|
|
4
7
|
baseUrl: string
|
|
5
|
-
headers:
|
|
8
|
+
headers: ApigenHeaders
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export interface ApigenRequest extends Omit<RequestInit, "body"> {
|
|
@@ -40,7 +43,7 @@ export class ApiClient {
|
|
|
40
43
|
}
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
|
|
46
|
+
PrepareFetchUrl(path: string): URL {
|
|
44
47
|
let base = this.Config.baseUrl
|
|
45
48
|
if ("location" in globalThis && (base === "" || base.startsWith("/"))) {
|
|
46
49
|
// make ts happy in pure nodejs environment, should never pass here
|
|
@@ -48,12 +51,22 @@ export class ApiClient {
|
|
|
48
51
|
base = `${location.origin}${base.endsWith("/") ? base : `/${base}`}`
|
|
49
52
|
}
|
|
50
53
|
|
|
51
|
-
|
|
54
|
+
return new URL(path, base)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async Fetch<T>(method: string, path: string, opts: ApigenRequest = {}): Promise<T> {
|
|
58
|
+
const url = this.PrepareFetchUrl(path)
|
|
59
|
+
|
|
52
60
|
for (const [k, v] of Object.entries(opts?.search ?? {})) {
|
|
53
61
|
url.searchParams.append(k, Array.isArray(v) ? v.join(",") : (v as string))
|
|
54
62
|
}
|
|
55
63
|
|
|
56
|
-
const
|
|
64
|
+
const configHeaders =
|
|
65
|
+
typeof this.Config.headers === "function"
|
|
66
|
+
? await this.Config.headers(method, path)
|
|
67
|
+
: this.Config.headers
|
|
68
|
+
|
|
69
|
+
const headers = new Headers({ ...configHeaders, ...opts.headers })
|
|
57
70
|
const ct = headers.get("content-type") ?? "application/json"
|
|
58
71
|
|
|
59
72
|
let body: FormData | URLSearchParams | string | undefined = undefined
|
package/dist/cli.cjs
CHANGED
package/dist/cli.js
CHANGED
package/dist/cli.mjs
CHANGED
|
@@ -4,11 +4,14 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { cli } from 'cleye';
|
|
5
5
|
import redocly from '@redocly/openapi-core';
|
|
6
6
|
import { filterEmpty, filterNullable } from 'array-utils-ts';
|
|
7
|
-
import { get, uniq, upperFirst, isArray, isObject, sortBy, lowerFirst, uniqBy } from 'lodash-es';
|
|
7
|
+
import { get, uniq, upperFirst, isArray, isBoolean, isObject, sortBy, lowerFirst, uniqBy } from 'lodash-es';
|
|
8
8
|
import { convertObj } from 'swagger2openapi';
|
|
9
9
|
import ts from 'typescript';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
+
var name = "apigen-ts";
|
|
13
|
+
var version = "1.0.0";
|
|
14
|
+
|
|
12
15
|
const initCtx = (config) => {
|
|
13
16
|
return {
|
|
14
17
|
source: "",
|
|
@@ -23,8 +26,8 @@ const initCtx = (config) => {
|
|
|
23
26
|
};
|
|
24
27
|
const getCliConfig = () => {
|
|
25
28
|
const argv = cli({
|
|
26
|
-
name
|
|
27
|
-
version
|
|
29
|
+
name,
|
|
30
|
+
version,
|
|
28
31
|
parameters: ["<source>", "[output]"],
|
|
29
32
|
flags: {
|
|
30
33
|
name: {
|
|
@@ -49,8 +52,7 @@ const getCliConfig = () => {
|
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
const unref = (ctx, s) => {
|
|
52
|
-
if (!s)
|
|
53
|
-
return void 0;
|
|
55
|
+
if (!s) return void 0;
|
|
54
56
|
if ("$ref" in s && s.$ref) {
|
|
55
57
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
56
58
|
const obj = parts.reduce(
|
|
@@ -58,8 +60,7 @@ const unref = (ctx, s) => {
|
|
|
58
60
|
(acc, x) => get(acc, x, get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
|
|
59
61
|
ctx.doc
|
|
60
62
|
);
|
|
61
|
-
if (obj)
|
|
62
|
-
return obj;
|
|
63
|
+
if (obj) return obj;
|
|
63
64
|
console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
|
|
64
65
|
return void 0;
|
|
65
66
|
}
|
|
@@ -67,11 +68,9 @@ const unref = (ctx, s) => {
|
|
|
67
68
|
};
|
|
68
69
|
const getReqSchema = (ctx, config) => {
|
|
69
70
|
const req = unref(ctx, config.requestBody);
|
|
70
|
-
if (!req)
|
|
71
|
-
return void 0;
|
|
71
|
+
if (!req) return void 0;
|
|
72
72
|
const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
|
|
73
|
-
if (cts.length === 0)
|
|
74
|
-
return void 0;
|
|
73
|
+
if (cts.length === 0) return void 0;
|
|
75
74
|
const pretenders = [
|
|
76
75
|
"application/json",
|
|
77
76
|
"text/",
|
|
@@ -80,8 +79,7 @@ const getReqSchema = (ctx, config) => {
|
|
|
80
79
|
];
|
|
81
80
|
for (const p of pretenders) {
|
|
82
81
|
const ct = cts.find((x) => x[0].startsWith(p));
|
|
83
|
-
if (ct)
|
|
84
|
-
return ct;
|
|
82
|
+
if (ct) return ct;
|
|
85
83
|
}
|
|
86
84
|
cts.map((x) => x[0]);
|
|
87
85
|
return void 0;
|
|
@@ -89,14 +87,11 @@ const getReqSchema = (ctx, config) => {
|
|
|
89
87
|
const getRepSchema = (ctx, config) => {
|
|
90
88
|
const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => get(config, ["responses", x, "content"]));
|
|
91
89
|
const cts = Object.entries(get(config, ["responses", successCodes[0], "content"], {})).filter((x) => x[1].schema);
|
|
92
|
-
if (cts.length === 0)
|
|
93
|
-
return void 0;
|
|
90
|
+
if (cts.length === 0) return void 0;
|
|
94
91
|
const ctJson = cts.find((x) => x[0].startsWith("application/json"));
|
|
95
|
-
if (ctJson)
|
|
96
|
-
return ctJson[1].schema;
|
|
92
|
+
if (ctJson) return ctJson[1].schema;
|
|
97
93
|
const ctText = cts.find((x) => x[0].startsWith("text/"));
|
|
98
|
-
if (ctText)
|
|
99
|
-
return { type: "string" };
|
|
94
|
+
if (ctText) return { type: "string" };
|
|
100
95
|
cts.map((x) => x[0]).join(", ");
|
|
101
96
|
return void 0;
|
|
102
97
|
};
|
|
@@ -169,25 +164,18 @@ const Keywords = /* @__PURE__ */ new Set([
|
|
|
169
164
|
]);
|
|
170
165
|
const normalizeIdentifier = (val, asVar = false) => {
|
|
171
166
|
let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
172
|
-
if (name.match(/^\d/))
|
|
173
|
-
|
|
174
|
-
if (asVar && Keywords.has(name))
|
|
175
|
-
name = `$${name}`;
|
|
167
|
+
if (name.match(/^\d/)) name = `$${name}`;
|
|
168
|
+
if (asVar && Keywords.has(name)) name = `$${name}`;
|
|
176
169
|
return name;
|
|
177
170
|
};
|
|
178
171
|
const makeInlineEnum = (s) => {
|
|
179
|
-
if (!s.enum)
|
|
180
|
-
return void 0;
|
|
172
|
+
if (!s.enum) return void 0;
|
|
181
173
|
const values = filterEmpty(s.enum);
|
|
182
|
-
if (!values.length)
|
|
183
|
-
return void 0;
|
|
174
|
+
if (!values.length) return void 0;
|
|
184
175
|
if (!s.type) {
|
|
185
|
-
if (values.every((x) => typeof x === "string"))
|
|
186
|
-
|
|
187
|
-
if (values.every((x) => typeof x === "
|
|
188
|
-
s.type = "number";
|
|
189
|
-
if (values.every((x) => typeof x === "boolean"))
|
|
190
|
-
s.type = "boolean";
|
|
176
|
+
if (values.every((x) => typeof x === "string")) s.type = "string";
|
|
177
|
+
if (values.every((x) => typeof x === "number")) s.type = "number";
|
|
178
|
+
if (values.every((x) => typeof x === "boolean")) s.type = "boolean";
|
|
191
179
|
}
|
|
192
180
|
if (s.type === "string") {
|
|
193
181
|
const tokens = uniq(values).map((x) => f$2.createStringLiteral(x.toString()));
|
|
@@ -199,44 +187,44 @@ const makeInlineEnum = (s) => {
|
|
|
199
187
|
}
|
|
200
188
|
if (s.type === "boolean") {
|
|
201
189
|
const tokens = [];
|
|
202
|
-
if (values.includes(true))
|
|
203
|
-
|
|
204
|
-
if (values.includes(false))
|
|
205
|
-
tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
190
|
+
if (values.includes(true)) tokens.push(f$2.createToken(ts.SyntaxKind.TrueKeyword));
|
|
191
|
+
if (values.includes(false)) tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
206
192
|
return f$2.createUnionTypeNode(tokens.map((x) => f$2.createLiteralTypeNode(x)));
|
|
207
193
|
}
|
|
208
194
|
console.warn(`enum with unknown type "${s.type}" in`, s);
|
|
209
195
|
return void 0;
|
|
210
196
|
};
|
|
197
|
+
const makeObject = (ctx, s) => {
|
|
198
|
+
if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
|
|
199
|
+
if (s.additionalProperties && !isBoolean(s.additionalProperties)) {
|
|
200
|
+
return f$2.createTypeReferenceNode("Record", [
|
|
201
|
+
f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
202
|
+
makeType(ctx, s.additionalProperties)
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
|
|
206
|
+
};
|
|
211
207
|
const makeType = (ctx, s) => {
|
|
212
208
|
const mk = makeType.bind(null, ctx);
|
|
213
|
-
if (s === void 0)
|
|
214
|
-
|
|
215
|
-
if (s === null)
|
|
216
|
-
return f$2.createLiteralTypeNode(f$2.createNull());
|
|
209
|
+
if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
|
|
210
|
+
if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
|
|
217
211
|
if ("$ref" in s && s.$ref) {
|
|
218
212
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
219
213
|
if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
|
|
220
214
|
return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
|
|
221
215
|
}
|
|
222
216
|
const t = unref(ctx, s);
|
|
223
|
-
if (!t)
|
|
224
|
-
throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
217
|
+
if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
225
218
|
return makeType(ctx, t);
|
|
226
219
|
}
|
|
227
|
-
if ("oneOf" in s && s.oneOf)
|
|
228
|
-
|
|
229
|
-
if ("
|
|
230
|
-
|
|
231
|
-
if ("allOf" in s && s.allOf)
|
|
232
|
-
return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
233
|
-
if ("type" in s && s.type === "integer")
|
|
234
|
-
s.type = "number";
|
|
220
|
+
if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
|
|
221
|
+
if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
|
|
222
|
+
if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
223
|
+
if ("type" in s && s.type === "integer") s.type = "number";
|
|
235
224
|
if ("enum" in s && s.enum && !Array.isArray(s.type)) {
|
|
236
225
|
const isArray2 = s.type === "array";
|
|
237
226
|
const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
|
|
238
|
-
if (t)
|
|
239
|
-
return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
227
|
+
if (t) return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
240
228
|
}
|
|
241
229
|
if ("properties" in s && s.properties) {
|
|
242
230
|
return f$2.createTypeLiteralNode(
|
|
@@ -251,37 +239,26 @@ const makeType = (ctx, s) => {
|
|
|
251
239
|
if (Array.isArray(s.type)) {
|
|
252
240
|
const types = [];
|
|
253
241
|
for (const type of s.type) {
|
|
254
|
-
if (type === "null")
|
|
255
|
-
|
|
256
|
-
else
|
|
257
|
-
types.push({ ...s, type });
|
|
242
|
+
if (type === "null") types.push({ type: "null" });
|
|
243
|
+
else types.push({ ...s, type });
|
|
258
244
|
}
|
|
259
245
|
return mk({ oneOf: types });
|
|
260
246
|
}
|
|
261
247
|
let t;
|
|
262
|
-
if (s.type === "object")
|
|
263
|
-
|
|
264
|
-
else if (s.type === "
|
|
265
|
-
|
|
266
|
-
else if (s.type === "
|
|
267
|
-
|
|
268
|
-
else if (s.type
|
|
269
|
-
t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
270
|
-
else if (s.type === "array")
|
|
271
|
-
t = f$2.createArrayTypeNode(mk(s.items));
|
|
272
|
-
else if (s.type === "null")
|
|
273
|
-
t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
274
|
-
else if (isArray(s.type))
|
|
275
|
-
t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
248
|
+
if (s.type === "object") t = makeObject(ctx, s);
|
|
249
|
+
else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
250
|
+
else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
251
|
+
else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
252
|
+
else if (s.type === "array") t = f$2.createArrayTypeNode(mk(s.items));
|
|
253
|
+
else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
254
|
+
else if (isArray(s.type)) t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
276
255
|
else {
|
|
277
256
|
console.warn(`makeType: unknown type "${s.type}"`);
|
|
278
257
|
return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
279
258
|
}
|
|
280
259
|
if (s.type === "string") {
|
|
281
|
-
if (s.format === "binary")
|
|
282
|
-
|
|
283
|
-
if (s.format === "date-time" && ctx.parseDates)
|
|
284
|
-
t = f$2.createTypeReferenceNode("Date");
|
|
260
|
+
if (s.format === "binary") t = f$2.createTypeReferenceNode("File");
|
|
261
|
+
if (s.format === "date-time" && ctx.parseDates) t = f$2.createTypeReferenceNode("Date");
|
|
285
262
|
}
|
|
286
263
|
return s.nullable ? f$2.createUnionTypeNode([t, f$2.createLiteralTypeNode(f$2.createNull())]) : t;
|
|
287
264
|
}
|
|
@@ -333,15 +310,13 @@ const getOpName = (ctx, op) => {
|
|
|
333
310
|
}
|
|
334
311
|
fn = normalizeOpName(fn);
|
|
335
312
|
let nsr = ns.split("").map((x) => `[${x.toUpperCase()}${x.toLowerCase()}]`).join("");
|
|
336
|
-
if (nsr.endsWith("[Ss]"))
|
|
337
|
-
nsr += "?";
|
|
313
|
+
if (nsr.endsWith("[Ss]")) nsr += "?";
|
|
338
314
|
fn = fn.replace(new RegExp(`^${nsr}([Cc]ontroller|[Ss]ervice)?([A-Z].*)$`), "$2");
|
|
339
315
|
fn = lowerFirst(fn);
|
|
340
316
|
const proposal = [ns, fn];
|
|
341
317
|
if (ctx.resolveName) {
|
|
342
318
|
const res = ctx.resolveName(ctx, op, proposal);
|
|
343
|
-
if (Array.isArray(res) && res.length === 2)
|
|
344
|
-
return res;
|
|
319
|
+
if (Array.isArray(res) && res.length === 2) return res;
|
|
345
320
|
if (res !== void 0) {
|
|
346
321
|
console.warn(`${ctx.logTag} resolveName should return [ns, fn] or undefined (skipping)`);
|
|
347
322
|
}
|
|
@@ -349,11 +324,9 @@ const getOpName = (ctx, op) => {
|
|
|
349
324
|
return proposal;
|
|
350
325
|
};
|
|
351
326
|
const prepareUrl = (url, rename) => {
|
|
352
|
-
for (const [k, v] of Object.entries(rename))
|
|
353
|
-
url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
327
|
+
for (const [k, v] of Object.entries(rename)) url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
354
328
|
const parts = url.split("${");
|
|
355
|
-
if (parts.length === 1)
|
|
356
|
-
return f$1.createStringLiteral(url);
|
|
329
|
+
if (parts.length === 1) return f$1.createStringLiteral(url);
|
|
357
330
|
return f$1.createTemplateExpression(
|
|
358
331
|
f$1.createTemplateHead(parts[0]),
|
|
359
332
|
parts.slice(1).map((x, i) => {
|
|
@@ -373,13 +346,11 @@ const prepareOp = (ctx, cfg, opName) => {
|
|
|
373
346
|
const repSchema = getRepSchema(ctx, cfg);
|
|
374
347
|
const allParams = filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
|
|
375
348
|
const params = uniqBy(allParams.filter((x) => x.in === "path"), "name");
|
|
376
|
-
if (reqSchema)
|
|
377
|
-
params.push({ name: "body", schema: reqSchema[1] });
|
|
349
|
+
if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
|
|
378
350
|
const search = allParams.filter((x) => x.in === "query");
|
|
379
351
|
allParams.filter((x) => x.in === "header");
|
|
380
352
|
for (const [name, v] of Object.entries({ search })) {
|
|
381
|
-
if (!v.length)
|
|
382
|
-
continue;
|
|
353
|
+
if (!v.length) continue;
|
|
383
354
|
const properties = v.reduce((acc, x) => ({ ...acc, [x.name]: x.schema }), {});
|
|
384
355
|
params.push({ name, schema: { type: "object", properties } });
|
|
385
356
|
}
|
|
@@ -443,8 +414,7 @@ const prepareRoutes = async (ctx) => {
|
|
|
443
414
|
const routes = {};
|
|
444
415
|
for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
|
|
445
416
|
ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
|
|
446
|
-
if (!isObject(pathConfig))
|
|
447
|
-
continue;
|
|
417
|
+
if (!isObject(pathConfig)) continue;
|
|
448
418
|
if ("$ref" in pathConfig) {
|
|
449
419
|
console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
|
|
450
420
|
continue;
|
|
@@ -452,14 +422,12 @@ const prepareRoutes = async (ctx) => {
|
|
|
452
422
|
for (const method of HttpMethods) {
|
|
453
423
|
ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
|
|
454
424
|
const config = pathConfig[method];
|
|
455
|
-
if (!config)
|
|
456
|
-
continue;
|
|
425
|
+
if (!config) continue;
|
|
457
426
|
if (pathConfig.parameters) {
|
|
458
427
|
config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
|
|
459
428
|
}
|
|
460
429
|
const [ns, op] = getOpName(ctx, { ...config, method, path });
|
|
461
|
-
if (!routes[ns])
|
|
462
|
-
routes[ns] = [];
|
|
430
|
+
if (!routes[ns]) routes[ns] = [];
|
|
463
431
|
const joined = [ns, op].join(".");
|
|
464
432
|
if (ctx.usedNames.has(joined)) {
|
|
465
433
|
continue;
|
|
@@ -499,8 +467,7 @@ const generateAst = async (ctx) => {
|
|
|
499
467
|
return { modules, types };
|
|
500
468
|
};
|
|
501
469
|
const loadSchema = async (url, upgrade = true) => {
|
|
502
|
-
if (url.startsWith("file://"))
|
|
503
|
-
url = url.substring(7);
|
|
470
|
+
if (url.startsWith("file://")) url = url.substring(7);
|
|
504
471
|
const { bundle } = await redocly.bundle({
|
|
505
472
|
ref: url,
|
|
506
473
|
config: await redocly.createConfig({}),
|
|
@@ -4,11 +4,14 @@ import { fileURLToPath } from 'url';
|
|
|
4
4
|
import { cli } from 'cleye';
|
|
5
5
|
import redocly from '@redocly/openapi-core';
|
|
6
6
|
import { filterEmpty, filterNullable } from 'array-utils-ts';
|
|
7
|
-
import { get, uniq, upperFirst, isArray, isObject, sortBy, lowerFirst, uniqBy } from 'lodash-es';
|
|
7
|
+
import { get, uniq, upperFirst, isArray, isBoolean, isObject, sortBy, lowerFirst, uniqBy } from 'lodash-es';
|
|
8
8
|
import { convertObj } from 'swagger2openapi';
|
|
9
9
|
import ts from 'typescript';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
|
|
12
|
+
var name = "apigen-ts";
|
|
13
|
+
var version = "1.0.0";
|
|
14
|
+
|
|
12
15
|
const initCtx = (config) => {
|
|
13
16
|
return {
|
|
14
17
|
source: "",
|
|
@@ -23,8 +26,8 @@ const initCtx = (config) => {
|
|
|
23
26
|
};
|
|
24
27
|
const getCliConfig = () => {
|
|
25
28
|
const argv = cli({
|
|
26
|
-
name
|
|
27
|
-
version
|
|
29
|
+
name,
|
|
30
|
+
version,
|
|
28
31
|
parameters: ["<source>", "[output]"],
|
|
29
32
|
flags: {
|
|
30
33
|
name: {
|
|
@@ -49,8 +52,7 @@ const getCliConfig = () => {
|
|
|
49
52
|
};
|
|
50
53
|
|
|
51
54
|
const unref = (ctx, s) => {
|
|
52
|
-
if (!s)
|
|
53
|
-
return void 0;
|
|
55
|
+
if (!s) return void 0;
|
|
54
56
|
if ("$ref" in s && s.$ref) {
|
|
55
57
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
56
58
|
const obj = parts.reduce(
|
|
@@ -58,8 +60,7 @@ const unref = (ctx, s) => {
|
|
|
58
60
|
(acc, x) => get(acc, x, get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
|
|
59
61
|
ctx.doc
|
|
60
62
|
);
|
|
61
|
-
if (obj)
|
|
62
|
-
return obj;
|
|
63
|
+
if (obj) return obj;
|
|
63
64
|
console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
|
|
64
65
|
return void 0;
|
|
65
66
|
}
|
|
@@ -67,11 +68,9 @@ const unref = (ctx, s) => {
|
|
|
67
68
|
};
|
|
68
69
|
const getReqSchema = (ctx, config) => {
|
|
69
70
|
const req = unref(ctx, config.requestBody);
|
|
70
|
-
if (!req)
|
|
71
|
-
return void 0;
|
|
71
|
+
if (!req) return void 0;
|
|
72
72
|
const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
|
|
73
|
-
if (cts.length === 0)
|
|
74
|
-
return void 0;
|
|
73
|
+
if (cts.length === 0) return void 0;
|
|
75
74
|
const pretenders = [
|
|
76
75
|
"application/json",
|
|
77
76
|
"text/",
|
|
@@ -80,8 +79,7 @@ const getReqSchema = (ctx, config) => {
|
|
|
80
79
|
];
|
|
81
80
|
for (const p of pretenders) {
|
|
82
81
|
const ct = cts.find((x) => x[0].startsWith(p));
|
|
83
|
-
if (ct)
|
|
84
|
-
return ct;
|
|
82
|
+
if (ct) return ct;
|
|
85
83
|
}
|
|
86
84
|
cts.map((x) => x[0]);
|
|
87
85
|
return void 0;
|
|
@@ -89,14 +87,11 @@ const getReqSchema = (ctx, config) => {
|
|
|
89
87
|
const getRepSchema = (ctx, config) => {
|
|
90
88
|
const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => get(config, ["responses", x, "content"]));
|
|
91
89
|
const cts = Object.entries(get(config, ["responses", successCodes[0], "content"], {})).filter((x) => x[1].schema);
|
|
92
|
-
if (cts.length === 0)
|
|
93
|
-
return void 0;
|
|
90
|
+
if (cts.length === 0) return void 0;
|
|
94
91
|
const ctJson = cts.find((x) => x[0].startsWith("application/json"));
|
|
95
|
-
if (ctJson)
|
|
96
|
-
return ctJson[1].schema;
|
|
92
|
+
if (ctJson) return ctJson[1].schema;
|
|
97
93
|
const ctText = cts.find((x) => x[0].startsWith("text/"));
|
|
98
|
-
if (ctText)
|
|
99
|
-
return { type: "string" };
|
|
94
|
+
if (ctText) return { type: "string" };
|
|
100
95
|
cts.map((x) => x[0]).join(", ");
|
|
101
96
|
return void 0;
|
|
102
97
|
};
|
|
@@ -169,25 +164,18 @@ const Keywords = /* @__PURE__ */ new Set([
|
|
|
169
164
|
]);
|
|
170
165
|
const normalizeIdentifier = (val, asVar = false) => {
|
|
171
166
|
let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
172
|
-
if (name.match(/^\d/))
|
|
173
|
-
|
|
174
|
-
if (asVar && Keywords.has(name))
|
|
175
|
-
name = `$${name}`;
|
|
167
|
+
if (name.match(/^\d/)) name = `$${name}`;
|
|
168
|
+
if (asVar && Keywords.has(name)) name = `$${name}`;
|
|
176
169
|
return name;
|
|
177
170
|
};
|
|
178
171
|
const makeInlineEnum = (s) => {
|
|
179
|
-
if (!s.enum)
|
|
180
|
-
return void 0;
|
|
172
|
+
if (!s.enum) return void 0;
|
|
181
173
|
const values = filterEmpty(s.enum);
|
|
182
|
-
if (!values.length)
|
|
183
|
-
return void 0;
|
|
174
|
+
if (!values.length) return void 0;
|
|
184
175
|
if (!s.type) {
|
|
185
|
-
if (values.every((x) => typeof x === "string"))
|
|
186
|
-
|
|
187
|
-
if (values.every((x) => typeof x === "
|
|
188
|
-
s.type = "number";
|
|
189
|
-
if (values.every((x) => typeof x === "boolean"))
|
|
190
|
-
s.type = "boolean";
|
|
176
|
+
if (values.every((x) => typeof x === "string")) s.type = "string";
|
|
177
|
+
if (values.every((x) => typeof x === "number")) s.type = "number";
|
|
178
|
+
if (values.every((x) => typeof x === "boolean")) s.type = "boolean";
|
|
191
179
|
}
|
|
192
180
|
if (s.type === "string") {
|
|
193
181
|
const tokens = uniq(values).map((x) => f$2.createStringLiteral(x.toString()));
|
|
@@ -199,44 +187,44 @@ const makeInlineEnum = (s) => {
|
|
|
199
187
|
}
|
|
200
188
|
if (s.type === "boolean") {
|
|
201
189
|
const tokens = [];
|
|
202
|
-
if (values.includes(true))
|
|
203
|
-
|
|
204
|
-
if (values.includes(false))
|
|
205
|
-
tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
190
|
+
if (values.includes(true)) tokens.push(f$2.createToken(ts.SyntaxKind.TrueKeyword));
|
|
191
|
+
if (values.includes(false)) tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
206
192
|
return f$2.createUnionTypeNode(tokens.map((x) => f$2.createLiteralTypeNode(x)));
|
|
207
193
|
}
|
|
208
194
|
console.warn(`enum with unknown type "${s.type}" in`, s);
|
|
209
195
|
return void 0;
|
|
210
196
|
};
|
|
197
|
+
const makeObject = (ctx, s) => {
|
|
198
|
+
if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
|
|
199
|
+
if (s.additionalProperties && !isBoolean(s.additionalProperties)) {
|
|
200
|
+
return f$2.createTypeReferenceNode("Record", [
|
|
201
|
+
f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
202
|
+
makeType(ctx, s.additionalProperties)
|
|
203
|
+
]);
|
|
204
|
+
}
|
|
205
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
|
|
206
|
+
};
|
|
211
207
|
const makeType = (ctx, s) => {
|
|
212
208
|
const mk = makeType.bind(null, ctx);
|
|
213
|
-
if (s === void 0)
|
|
214
|
-
|
|
215
|
-
if (s === null)
|
|
216
|
-
return f$2.createLiteralTypeNode(f$2.createNull());
|
|
209
|
+
if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
|
|
210
|
+
if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
|
|
217
211
|
if ("$ref" in s && s.$ref) {
|
|
218
212
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
219
213
|
if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
|
|
220
214
|
return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
|
|
221
215
|
}
|
|
222
216
|
const t = unref(ctx, s);
|
|
223
|
-
if (!t)
|
|
224
|
-
throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
217
|
+
if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
225
218
|
return makeType(ctx, t);
|
|
226
219
|
}
|
|
227
|
-
if ("oneOf" in s && s.oneOf)
|
|
228
|
-
|
|
229
|
-
if ("
|
|
230
|
-
|
|
231
|
-
if ("allOf" in s && s.allOf)
|
|
232
|
-
return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
233
|
-
if ("type" in s && s.type === "integer")
|
|
234
|
-
s.type = "number";
|
|
220
|
+
if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
|
|
221
|
+
if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
|
|
222
|
+
if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
223
|
+
if ("type" in s && s.type === "integer") s.type = "number";
|
|
235
224
|
if ("enum" in s && s.enum && !Array.isArray(s.type)) {
|
|
236
225
|
const isArray2 = s.type === "array";
|
|
237
226
|
const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
|
|
238
|
-
if (t)
|
|
239
|
-
return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
227
|
+
if (t) return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
240
228
|
}
|
|
241
229
|
if ("properties" in s && s.properties) {
|
|
242
230
|
return f$2.createTypeLiteralNode(
|
|
@@ -251,37 +239,26 @@ const makeType = (ctx, s) => {
|
|
|
251
239
|
if (Array.isArray(s.type)) {
|
|
252
240
|
const types = [];
|
|
253
241
|
for (const type of s.type) {
|
|
254
|
-
if (type === "null")
|
|
255
|
-
|
|
256
|
-
else
|
|
257
|
-
types.push({ ...s, type });
|
|
242
|
+
if (type === "null") types.push({ type: "null" });
|
|
243
|
+
else types.push({ ...s, type });
|
|
258
244
|
}
|
|
259
245
|
return mk({ oneOf: types });
|
|
260
246
|
}
|
|
261
247
|
let t;
|
|
262
|
-
if (s.type === "object")
|
|
263
|
-
|
|
264
|
-
else if (s.type === "
|
|
265
|
-
|
|
266
|
-
else if (s.type === "
|
|
267
|
-
|
|
268
|
-
else if (s.type
|
|
269
|
-
t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
270
|
-
else if (s.type === "array")
|
|
271
|
-
t = f$2.createArrayTypeNode(mk(s.items));
|
|
272
|
-
else if (s.type === "null")
|
|
273
|
-
t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
274
|
-
else if (isArray(s.type))
|
|
275
|
-
t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
248
|
+
if (s.type === "object") t = makeObject(ctx, s);
|
|
249
|
+
else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
250
|
+
else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
251
|
+
else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
252
|
+
else if (s.type === "array") t = f$2.createArrayTypeNode(mk(s.items));
|
|
253
|
+
else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
254
|
+
else if (isArray(s.type)) t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
276
255
|
else {
|
|
277
256
|
console.warn(`makeType: unknown type "${s.type}"`);
|
|
278
257
|
return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
279
258
|
}
|
|
280
259
|
if (s.type === "string") {
|
|
281
|
-
if (s.format === "binary")
|
|
282
|
-
|
|
283
|
-
if (s.format === "date-time" && ctx.parseDates)
|
|
284
|
-
t = f$2.createTypeReferenceNode("Date");
|
|
260
|
+
if (s.format === "binary") t = f$2.createTypeReferenceNode("File");
|
|
261
|
+
if (s.format === "date-time" && ctx.parseDates) t = f$2.createTypeReferenceNode("Date");
|
|
285
262
|
}
|
|
286
263
|
return s.nullable ? f$2.createUnionTypeNode([t, f$2.createLiteralTypeNode(f$2.createNull())]) : t;
|
|
287
264
|
}
|
|
@@ -333,15 +310,13 @@ const getOpName = (ctx, op) => {
|
|
|
333
310
|
}
|
|
334
311
|
fn = normalizeOpName(fn);
|
|
335
312
|
let nsr = ns.split("").map((x) => `[${x.toUpperCase()}${x.toLowerCase()}]`).join("");
|
|
336
|
-
if (nsr.endsWith("[Ss]"))
|
|
337
|
-
nsr += "?";
|
|
313
|
+
if (nsr.endsWith("[Ss]")) nsr += "?";
|
|
338
314
|
fn = fn.replace(new RegExp(`^${nsr}([Cc]ontroller|[Ss]ervice)?([A-Z].*)$`), "$2");
|
|
339
315
|
fn = lowerFirst(fn);
|
|
340
316
|
const proposal = [ns, fn];
|
|
341
317
|
if (ctx.resolveName) {
|
|
342
318
|
const res = ctx.resolveName(ctx, op, proposal);
|
|
343
|
-
if (Array.isArray(res) && res.length === 2)
|
|
344
|
-
return res;
|
|
319
|
+
if (Array.isArray(res) && res.length === 2) return res;
|
|
345
320
|
if (res !== void 0) {
|
|
346
321
|
console.warn(`${ctx.logTag} resolveName should return [ns, fn] or undefined (skipping)`);
|
|
347
322
|
}
|
|
@@ -349,11 +324,9 @@ const getOpName = (ctx, op) => {
|
|
|
349
324
|
return proposal;
|
|
350
325
|
};
|
|
351
326
|
const prepareUrl = (url, rename) => {
|
|
352
|
-
for (const [k, v] of Object.entries(rename))
|
|
353
|
-
url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
327
|
+
for (const [k, v] of Object.entries(rename)) url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
354
328
|
const parts = url.split("${");
|
|
355
|
-
if (parts.length === 1)
|
|
356
|
-
return f$1.createStringLiteral(url);
|
|
329
|
+
if (parts.length === 1) return f$1.createStringLiteral(url);
|
|
357
330
|
return f$1.createTemplateExpression(
|
|
358
331
|
f$1.createTemplateHead(parts[0]),
|
|
359
332
|
parts.slice(1).map((x, i) => {
|
|
@@ -373,13 +346,11 @@ const prepareOp = (ctx, cfg, opName) => {
|
|
|
373
346
|
const repSchema = getRepSchema(ctx, cfg);
|
|
374
347
|
const allParams = filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
|
|
375
348
|
const params = uniqBy(allParams.filter((x) => x.in === "path"), "name");
|
|
376
|
-
if (reqSchema)
|
|
377
|
-
params.push({ name: "body", schema: reqSchema[1] });
|
|
349
|
+
if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
|
|
378
350
|
const search = allParams.filter((x) => x.in === "query");
|
|
379
351
|
allParams.filter((x) => x.in === "header");
|
|
380
352
|
for (const [name, v] of Object.entries({ search })) {
|
|
381
|
-
if (!v.length)
|
|
382
|
-
continue;
|
|
353
|
+
if (!v.length) continue;
|
|
383
354
|
const properties = v.reduce((acc, x) => ({ ...acc, [x.name]: x.schema }), {});
|
|
384
355
|
params.push({ name, schema: { type: "object", properties } });
|
|
385
356
|
}
|
|
@@ -443,8 +414,7 @@ const prepareRoutes = async (ctx) => {
|
|
|
443
414
|
const routes = {};
|
|
444
415
|
for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
|
|
445
416
|
ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
|
|
446
|
-
if (!isObject(pathConfig))
|
|
447
|
-
continue;
|
|
417
|
+
if (!isObject(pathConfig)) continue;
|
|
448
418
|
if ("$ref" in pathConfig) {
|
|
449
419
|
console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
|
|
450
420
|
continue;
|
|
@@ -452,14 +422,12 @@ const prepareRoutes = async (ctx) => {
|
|
|
452
422
|
for (const method of HttpMethods) {
|
|
453
423
|
ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
|
|
454
424
|
const config = pathConfig[method];
|
|
455
|
-
if (!config)
|
|
456
|
-
continue;
|
|
425
|
+
if (!config) continue;
|
|
457
426
|
if (pathConfig.parameters) {
|
|
458
427
|
config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
|
|
459
428
|
}
|
|
460
429
|
const [ns, op] = getOpName(ctx, { ...config, method, path });
|
|
461
|
-
if (!routes[ns])
|
|
462
|
-
routes[ns] = [];
|
|
430
|
+
if (!routes[ns]) routes[ns] = [];
|
|
463
431
|
const joined = [ns, op].join(".");
|
|
464
432
|
if (ctx.usedNames.has(joined)) {
|
|
465
433
|
continue;
|
|
@@ -499,8 +467,7 @@ const generateAst = async (ctx) => {
|
|
|
499
467
|
return { modules, types };
|
|
500
468
|
};
|
|
501
469
|
const loadSchema = async (url, upgrade = true) => {
|
|
502
|
-
if (url.startsWith("file://"))
|
|
503
|
-
url = url.substring(7);
|
|
470
|
+
if (url.startsWith("file://")) url = url.substring(7);
|
|
504
471
|
const { bundle } = await redocly.bundle({
|
|
505
472
|
ref: url,
|
|
506
473
|
config: await redocly.createConfig({}),
|
|
@@ -12,6 +12,9 @@ var ts = require('typescript');
|
|
|
12
12
|
var path = require('node:path');
|
|
13
13
|
|
|
14
14
|
var _documentCurrentScript = typeof document !== 'undefined' ? document.currentScript : null;
|
|
15
|
+
var name = "apigen-ts";
|
|
16
|
+
var version = "1.0.0";
|
|
17
|
+
|
|
15
18
|
const initCtx = (config) => {
|
|
16
19
|
return {
|
|
17
20
|
source: "",
|
|
@@ -26,8 +29,8 @@ const initCtx = (config) => {
|
|
|
26
29
|
};
|
|
27
30
|
const getCliConfig = () => {
|
|
28
31
|
const argv = cleye.cli({
|
|
29
|
-
name
|
|
30
|
-
version
|
|
32
|
+
name,
|
|
33
|
+
version,
|
|
31
34
|
parameters: ["<source>", "[output]"],
|
|
32
35
|
flags: {
|
|
33
36
|
name: {
|
|
@@ -52,8 +55,7 @@ const getCliConfig = () => {
|
|
|
52
55
|
};
|
|
53
56
|
|
|
54
57
|
const unref = (ctx, s) => {
|
|
55
|
-
if (!s)
|
|
56
|
-
return void 0;
|
|
58
|
+
if (!s) return void 0;
|
|
57
59
|
if ("$ref" in s && s.$ref) {
|
|
58
60
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
59
61
|
const obj = parts.reduce(
|
|
@@ -61,8 +63,7 @@ const unref = (ctx, s) => {
|
|
|
61
63
|
(acc, x) => lodashEs.get(acc, x, lodashEs.get(acc, decodeURIComponent(x).replaceAll("~1", "/"))),
|
|
62
64
|
ctx.doc
|
|
63
65
|
);
|
|
64
|
-
if (obj)
|
|
65
|
-
return obj;
|
|
66
|
+
if (obj) return obj;
|
|
66
67
|
console.warn(`${ctx.logTag} ref ${s.$ref} not found`);
|
|
67
68
|
return void 0;
|
|
68
69
|
}
|
|
@@ -70,11 +71,9 @@ const unref = (ctx, s) => {
|
|
|
70
71
|
};
|
|
71
72
|
const getReqSchema = (ctx, config) => {
|
|
72
73
|
const req = unref(ctx, config.requestBody);
|
|
73
|
-
if (!req)
|
|
74
|
-
return void 0;
|
|
74
|
+
if (!req) return void 0;
|
|
75
75
|
const cts = Object.entries(req.content ?? {}).map((x) => [x[0].split(";")[0], x[1].schema]).filter((x) => x[1]);
|
|
76
|
-
if (cts.length === 0)
|
|
77
|
-
return void 0;
|
|
76
|
+
if (cts.length === 0) return void 0;
|
|
78
77
|
const pretenders = [
|
|
79
78
|
"application/json",
|
|
80
79
|
"text/",
|
|
@@ -83,8 +82,7 @@ const getReqSchema = (ctx, config) => {
|
|
|
83
82
|
];
|
|
84
83
|
for (const p of pretenders) {
|
|
85
84
|
const ct = cts.find((x) => x[0].startsWith(p));
|
|
86
|
-
if (ct)
|
|
87
|
-
return ct;
|
|
85
|
+
if (ct) return ct;
|
|
88
86
|
}
|
|
89
87
|
cts.map((x) => x[0]);
|
|
90
88
|
return void 0;
|
|
@@ -92,14 +90,11 @@ const getReqSchema = (ctx, config) => {
|
|
|
92
90
|
const getRepSchema = (ctx, config) => {
|
|
93
91
|
const successCodes = Object.keys(config.responses ?? {}).filter((x) => x.startsWith("2")).filter((x) => lodashEs.get(config, ["responses", x, "content"]));
|
|
94
92
|
const cts = Object.entries(lodashEs.get(config, ["responses", successCodes[0], "content"], {})).filter((x) => x[1].schema);
|
|
95
|
-
if (cts.length === 0)
|
|
96
|
-
return void 0;
|
|
93
|
+
if (cts.length === 0) return void 0;
|
|
97
94
|
const ctJson = cts.find((x) => x[0].startsWith("application/json"));
|
|
98
|
-
if (ctJson)
|
|
99
|
-
return ctJson[1].schema;
|
|
95
|
+
if (ctJson) return ctJson[1].schema;
|
|
100
96
|
const ctText = cts.find((x) => x[0].startsWith("text/"));
|
|
101
|
-
if (ctText)
|
|
102
|
-
return { type: "string" };
|
|
97
|
+
if (ctText) return { type: "string" };
|
|
103
98
|
cts.map((x) => x[0]).join(", ");
|
|
104
99
|
return void 0;
|
|
105
100
|
};
|
|
@@ -172,25 +167,18 @@ const Keywords = /* @__PURE__ */ new Set([
|
|
|
172
167
|
]);
|
|
173
168
|
const normalizeIdentifier = (val, asVar = false) => {
|
|
174
169
|
let name = val.replace("#/components/schemas/", "").replaceAll("'", "").replace(/[^a-zA-Z0-9]/g, "_");
|
|
175
|
-
if (name.match(/^\d/))
|
|
176
|
-
|
|
177
|
-
if (asVar && Keywords.has(name))
|
|
178
|
-
name = `$${name}`;
|
|
170
|
+
if (name.match(/^\d/)) name = `$${name}`;
|
|
171
|
+
if (asVar && Keywords.has(name)) name = `$${name}`;
|
|
179
172
|
return name;
|
|
180
173
|
};
|
|
181
174
|
const makeInlineEnum = (s) => {
|
|
182
|
-
if (!s.enum)
|
|
183
|
-
return void 0;
|
|
175
|
+
if (!s.enum) return void 0;
|
|
184
176
|
const values = arrayUtilsTs.filterEmpty(s.enum);
|
|
185
|
-
if (!values.length)
|
|
186
|
-
return void 0;
|
|
177
|
+
if (!values.length) return void 0;
|
|
187
178
|
if (!s.type) {
|
|
188
|
-
if (values.every((x) => typeof x === "string"))
|
|
189
|
-
|
|
190
|
-
if (values.every((x) => typeof x === "
|
|
191
|
-
s.type = "number";
|
|
192
|
-
if (values.every((x) => typeof x === "boolean"))
|
|
193
|
-
s.type = "boolean";
|
|
179
|
+
if (values.every((x) => typeof x === "string")) s.type = "string";
|
|
180
|
+
if (values.every((x) => typeof x === "number")) s.type = "number";
|
|
181
|
+
if (values.every((x) => typeof x === "boolean")) s.type = "boolean";
|
|
194
182
|
}
|
|
195
183
|
if (s.type === "string") {
|
|
196
184
|
const tokens = lodashEs.uniq(values).map((x) => f$2.createStringLiteral(x.toString()));
|
|
@@ -202,44 +190,44 @@ const makeInlineEnum = (s) => {
|
|
|
202
190
|
}
|
|
203
191
|
if (s.type === "boolean") {
|
|
204
192
|
const tokens = [];
|
|
205
|
-
if (values.includes(true))
|
|
206
|
-
|
|
207
|
-
if (values.includes(false))
|
|
208
|
-
tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
193
|
+
if (values.includes(true)) tokens.push(f$2.createToken(ts.SyntaxKind.TrueKeyword));
|
|
194
|
+
if (values.includes(false)) tokens.push(f$2.createToken(ts.SyntaxKind.FalseKeyword));
|
|
209
195
|
return f$2.createUnionTypeNode(tokens.map((x) => f$2.createLiteralTypeNode(x)));
|
|
210
196
|
}
|
|
211
197
|
console.warn(`enum with unknown type "${s.type}" in`, s);
|
|
212
198
|
return void 0;
|
|
213
199
|
};
|
|
200
|
+
const makeObject = (ctx, s) => {
|
|
201
|
+
if (s.type !== "object") throw new Error(`makeObject: not an object ${JSON.stringify(s)}`);
|
|
202
|
+
if (s.additionalProperties && !lodashEs.isBoolean(s.additionalProperties)) {
|
|
203
|
+
return f$2.createTypeReferenceNode("Record", [
|
|
204
|
+
f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
|
|
205
|
+
makeType(ctx, s.additionalProperties)
|
|
206
|
+
]);
|
|
207
|
+
}
|
|
208
|
+
return f$2.createKeywordTypeNode(ts.SyntaxKind.ObjectKeyword);
|
|
209
|
+
};
|
|
214
210
|
const makeType = (ctx, s) => {
|
|
215
211
|
const mk = makeType.bind(null, ctx);
|
|
216
|
-
if (s === void 0)
|
|
217
|
-
|
|
218
|
-
if (s === null)
|
|
219
|
-
return f$2.createLiteralTypeNode(f$2.createNull());
|
|
212
|
+
if (s === void 0) return f$2.createKeywordTypeNode(ts.SyntaxKind.VoidKeyword);
|
|
213
|
+
if (s === null) return f$2.createLiteralTypeNode(f$2.createNull());
|
|
220
214
|
if ("$ref" in s && s.$ref) {
|
|
221
215
|
const parts = s.$ref.replace("#/", "").split("/");
|
|
222
216
|
if (parts.length === 3 && parts[0] === "components" && parts[1] === "schemas") {
|
|
223
217
|
return f$2.createTypeReferenceNode(normalizeIdentifier(parts[2], true));
|
|
224
218
|
}
|
|
225
219
|
const t = unref(ctx, s);
|
|
226
|
-
if (!t)
|
|
227
|
-
throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
220
|
+
if (!t) throw new Error(`makeTypeRef: ref not found ${JSON.stringify(s)}`);
|
|
228
221
|
return makeType(ctx, t);
|
|
229
222
|
}
|
|
230
|
-
if ("oneOf" in s && s.oneOf)
|
|
231
|
-
|
|
232
|
-
if ("
|
|
233
|
-
|
|
234
|
-
if ("allOf" in s && s.allOf)
|
|
235
|
-
return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
236
|
-
if ("type" in s && s.type === "integer")
|
|
237
|
-
s.type = "number";
|
|
223
|
+
if ("oneOf" in s && s.oneOf) return f$2.createUnionTypeNode(s.oneOf.map(mk));
|
|
224
|
+
if ("anyOf" in s && s.anyOf) return f$2.createUnionTypeNode(s.anyOf.map(mk));
|
|
225
|
+
if ("allOf" in s && s.allOf) return f$2.createIntersectionTypeNode(s.allOf.map(mk));
|
|
226
|
+
if ("type" in s && s.type === "integer") s.type = "number";
|
|
238
227
|
if ("enum" in s && s.enum && !Array.isArray(s.type)) {
|
|
239
228
|
const isArray2 = s.type === "array";
|
|
240
229
|
const t = makeInlineEnum(isArray2 ? { ...s, type: s.items?.type } : s);
|
|
241
|
-
if (t)
|
|
242
|
-
return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
230
|
+
if (t) return isArray2 ? f$2.createArrayTypeNode(t) : t;
|
|
243
231
|
}
|
|
244
232
|
if ("properties" in s && s.properties) {
|
|
245
233
|
return f$2.createTypeLiteralNode(
|
|
@@ -254,37 +242,26 @@ const makeType = (ctx, s) => {
|
|
|
254
242
|
if (Array.isArray(s.type)) {
|
|
255
243
|
const types = [];
|
|
256
244
|
for (const type of s.type) {
|
|
257
|
-
if (type === "null")
|
|
258
|
-
|
|
259
|
-
else
|
|
260
|
-
types.push({ ...s, type });
|
|
245
|
+
if (type === "null") types.push({ type: "null" });
|
|
246
|
+
else types.push({ ...s, type });
|
|
261
247
|
}
|
|
262
248
|
return mk({ oneOf: types });
|
|
263
249
|
}
|
|
264
250
|
let t;
|
|
265
|
-
if (s.type === "object")
|
|
266
|
-
|
|
267
|
-
else if (s.type === "
|
|
268
|
-
|
|
269
|
-
else if (s.type === "
|
|
270
|
-
|
|
271
|
-
else if (s.type
|
|
272
|
-
t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
273
|
-
else if (s.type === "array")
|
|
274
|
-
t = f$2.createArrayTypeNode(mk(s.items));
|
|
275
|
-
else if (s.type === "null")
|
|
276
|
-
t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
277
|
-
else if (lodashEs.isArray(s.type))
|
|
278
|
-
t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
251
|
+
if (s.type === "object") t = makeObject(ctx, s);
|
|
252
|
+
else if (s.type === "boolean") t = f$2.createKeywordTypeNode(ts.SyntaxKind.BooleanKeyword);
|
|
253
|
+
else if (s.type === "number") t = f$2.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword);
|
|
254
|
+
else if (s.type === "string") t = f$2.createKeywordTypeNode(ts.SyntaxKind.StringKeyword);
|
|
255
|
+
else if (s.type === "array") t = f$2.createArrayTypeNode(mk(s.items));
|
|
256
|
+
else if (s.type === "null") t = f$2.createLiteralTypeNode(f$2.createNull());
|
|
257
|
+
else if (lodashEs.isArray(s.type)) t = f$2.createUnionTypeNode(s.type.map((x) => mk({ type: x })));
|
|
279
258
|
else {
|
|
280
259
|
console.warn(`makeType: unknown type "${s.type}"`);
|
|
281
260
|
return f$2.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword);
|
|
282
261
|
}
|
|
283
262
|
if (s.type === "string") {
|
|
284
|
-
if (s.format === "binary")
|
|
285
|
-
|
|
286
|
-
if (s.format === "date-time" && ctx.parseDates)
|
|
287
|
-
t = f$2.createTypeReferenceNode("Date");
|
|
263
|
+
if (s.format === "binary") t = f$2.createTypeReferenceNode("File");
|
|
264
|
+
if (s.format === "date-time" && ctx.parseDates) t = f$2.createTypeReferenceNode("Date");
|
|
288
265
|
}
|
|
289
266
|
return s.nullable ? f$2.createUnionTypeNode([t, f$2.createLiteralTypeNode(f$2.createNull())]) : t;
|
|
290
267
|
}
|
|
@@ -336,15 +313,13 @@ const getOpName = (ctx, op) => {
|
|
|
336
313
|
}
|
|
337
314
|
fn = normalizeOpName(fn);
|
|
338
315
|
let nsr = ns.split("").map((x) => `[${x.toUpperCase()}${x.toLowerCase()}]`).join("");
|
|
339
|
-
if (nsr.endsWith("[Ss]"))
|
|
340
|
-
nsr += "?";
|
|
316
|
+
if (nsr.endsWith("[Ss]")) nsr += "?";
|
|
341
317
|
fn = fn.replace(new RegExp(`^${nsr}([Cc]ontroller|[Ss]ervice)?([A-Z].*)$`), "$2");
|
|
342
318
|
fn = lodashEs.lowerFirst(fn);
|
|
343
319
|
const proposal = [ns, fn];
|
|
344
320
|
if (ctx.resolveName) {
|
|
345
321
|
const res = ctx.resolveName(ctx, op, proposal);
|
|
346
|
-
if (Array.isArray(res) && res.length === 2)
|
|
347
|
-
return res;
|
|
322
|
+
if (Array.isArray(res) && res.length === 2) return res;
|
|
348
323
|
if (res !== void 0) {
|
|
349
324
|
console.warn(`${ctx.logTag} resolveName should return [ns, fn] or undefined (skipping)`);
|
|
350
325
|
}
|
|
@@ -352,11 +327,9 @@ const getOpName = (ctx, op) => {
|
|
|
352
327
|
return proposal;
|
|
353
328
|
};
|
|
354
329
|
const prepareUrl = (url, rename) => {
|
|
355
|
-
for (const [k, v] of Object.entries(rename))
|
|
356
|
-
url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
330
|
+
for (const [k, v] of Object.entries(rename)) url = url.replaceAll(`{${k}}`, "${" + v + "}");
|
|
357
331
|
const parts = url.split("${");
|
|
358
|
-
if (parts.length === 1)
|
|
359
|
-
return f$1.createStringLiteral(url);
|
|
332
|
+
if (parts.length === 1) return f$1.createStringLiteral(url);
|
|
360
333
|
return f$1.createTemplateExpression(
|
|
361
334
|
f$1.createTemplateHead(parts[0]),
|
|
362
335
|
parts.slice(1).map((x, i) => {
|
|
@@ -376,13 +349,11 @@ const prepareOp = (ctx, cfg, opName) => {
|
|
|
376
349
|
const repSchema = getRepSchema(ctx, cfg);
|
|
377
350
|
const allParams = arrayUtilsTs.filterNullable(cfg.parameters.map((x) => unref(ctx, x)));
|
|
378
351
|
const params = lodashEs.uniqBy(allParams.filter((x) => x.in === "path"), "name");
|
|
379
|
-
if (reqSchema)
|
|
380
|
-
params.push({ name: "body", schema: reqSchema[1] });
|
|
352
|
+
if (reqSchema) params.push({ name: "body", schema: reqSchema[1] });
|
|
381
353
|
const search = allParams.filter((x) => x.in === "query");
|
|
382
354
|
allParams.filter((x) => x.in === "header");
|
|
383
355
|
for (const [name, v] of Object.entries({ search })) {
|
|
384
|
-
if (!v.length)
|
|
385
|
-
continue;
|
|
356
|
+
if (!v.length) continue;
|
|
386
357
|
const properties = v.reduce((acc, x) => ({ ...acc, [x.name]: x.schema }), {});
|
|
387
358
|
params.push({ name, schema: { type: "object", properties } });
|
|
388
359
|
}
|
|
@@ -446,8 +417,7 @@ const prepareRoutes = async (ctx) => {
|
|
|
446
417
|
const routes = {};
|
|
447
418
|
for (const [path, pathConfig] of Object.entries(ctx.doc.paths ?? {})) {
|
|
448
419
|
ctx.logTag = `${"[ALL]".toUpperCase().padEnd(6, " ")} ${path}`;
|
|
449
|
-
if (!lodashEs.isObject(pathConfig))
|
|
450
|
-
continue;
|
|
420
|
+
if (!lodashEs.isObject(pathConfig)) continue;
|
|
451
421
|
if ("$ref" in pathConfig) {
|
|
452
422
|
console.warn(`${ctx.logTag} $ref should be resolved before (skipping)`);
|
|
453
423
|
continue;
|
|
@@ -455,14 +425,12 @@ const prepareRoutes = async (ctx) => {
|
|
|
455
425
|
for (const method of HttpMethods) {
|
|
456
426
|
ctx.logTag = `${method.toUpperCase().padEnd(6, " ")} ${path}`;
|
|
457
427
|
const config = pathConfig[method];
|
|
458
|
-
if (!config)
|
|
459
|
-
continue;
|
|
428
|
+
if (!config) continue;
|
|
460
429
|
if (pathConfig.parameters) {
|
|
461
430
|
config.parameters = [...config.parameters ?? [], ...pathConfig.parameters];
|
|
462
431
|
}
|
|
463
432
|
const [ns, op] = getOpName(ctx, { ...config, method, path });
|
|
464
|
-
if (!routes[ns])
|
|
465
|
-
routes[ns] = [];
|
|
433
|
+
if (!routes[ns]) routes[ns] = [];
|
|
466
434
|
const joined = [ns, op].join(".");
|
|
467
435
|
if (ctx.usedNames.has(joined)) {
|
|
468
436
|
continue;
|
|
@@ -502,8 +470,7 @@ const generateAst = async (ctx) => {
|
|
|
502
470
|
return { modules, types };
|
|
503
471
|
};
|
|
504
472
|
const loadSchema = async (url, upgrade = true) => {
|
|
505
|
-
if (url.startsWith("file://"))
|
|
506
|
-
url = url.substring(7);
|
|
473
|
+
if (url.startsWith("file://")) url = url.substring(7);
|
|
507
474
|
const { bundle } = await redocly.bundle({
|
|
508
475
|
ref: url,
|
|
509
476
|
config: await redocly.createConfig({}),
|
|
@@ -548,7 +515,7 @@ const apigen = async (config) => {
|
|
|
548
515
|
const doc = await loadSchema(config.source);
|
|
549
516
|
const ctx = initCtx({ ...config, doc });
|
|
550
517
|
const { modules, types } = await generateAst(ctx);
|
|
551
|
-
const filepath = path$1.join(path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('main-
|
|
518
|
+
const filepath = path$1.join(path$1.dirname(url.fileURLToPath((typeof document === 'undefined' ? require('u' + 'rl').pathToFileURL(__filename).href : (_documentCurrentScript && _documentCurrentScript.src || new URL('main-oWgfChRG.cjs', document.baseURI).href)))), "_template.ts");
|
|
552
519
|
const file = await fs.readFile(filepath, "utf-8");
|
|
553
520
|
let code = [
|
|
554
521
|
`// Auto-generated by https://github.com/vladkens/apigen-ts`,
|
package/dist/main.cjs
CHANGED
package/dist/main.js
CHANGED
package/dist/main.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "apigen-ts",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "1.0.0",
|
|
5
5
|
"license": "MIT",
|
|
6
|
-
"author": "
|
|
6
|
+
"author": "vladkens <v.pronsky@gmail.com>",
|
|
7
7
|
"repository": "vladkens/apigen-ts",
|
|
8
|
-
"description": "OpenAPI
|
|
8
|
+
"description": "Simple typed OpenAPI client generator",
|
|
9
9
|
"keywords": [
|
|
10
10
|
"openapi",
|
|
11
11
|
"swagger",
|
|
@@ -22,24 +22,24 @@
|
|
|
22
22
|
"ci": "tsc --noEmit && yarn test-cov && yarn build"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@redocly/openapi-core": "
|
|
26
|
-
"@types/lodash-es": "
|
|
27
|
-
"@types/swagger2openapi": "
|
|
28
|
-
"array-utils-ts": "
|
|
29
|
-
"cleye": "
|
|
30
|
-
"lodash-es": "
|
|
31
|
-
"swagger2openapi": "
|
|
25
|
+
"@redocly/openapi-core": "1.25.5",
|
|
26
|
+
"@types/lodash-es": "4.17.12",
|
|
27
|
+
"@types/swagger2openapi": "7.0.4",
|
|
28
|
+
"array-utils-ts": "0.1.2",
|
|
29
|
+
"cleye": "1.3.2",
|
|
30
|
+
"lodash-es": "4.17.21",
|
|
31
|
+
"swagger2openapi": "7.0.8"
|
|
32
32
|
},
|
|
33
33
|
"devDependencies": {
|
|
34
|
-
"@types/node": "
|
|
35
|
-
"c8": "
|
|
36
|
-
"fetch-mock": "
|
|
37
|
-
"pkgroll": "
|
|
38
|
-
"prettier": "
|
|
39
|
-
"prettier-plugin-organize-imports": "
|
|
40
|
-
"tsm": "
|
|
41
|
-
"typescript": "
|
|
42
|
-
"uvu": "
|
|
34
|
+
"@types/node": "22.7.5",
|
|
35
|
+
"c8": "10.1.2",
|
|
36
|
+
"fetch-mock": "11.1.5",
|
|
37
|
+
"pkgroll": "2.5.0",
|
|
38
|
+
"prettier": "3.3.3",
|
|
39
|
+
"prettier-plugin-organize-imports": "4.1.0",
|
|
40
|
+
"tsm": "2.3.0",
|
|
41
|
+
"typescript": "5.6.3",
|
|
42
|
+
"uvu": "0.5.6"
|
|
43
43
|
},
|
|
44
44
|
"peerDependencies": {
|
|
45
45
|
"prettier": "^3.0.0",
|
package/readme.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
|
|
13
13
|
<div align="center">
|
|
14
14
|
<img src="./logo.svg" alt="apigen-ts logo" height="80" />
|
|
15
|
-
<div>
|
|
15
|
+
<div>Simple typed OpenAPI client generator</div>
|
|
16
16
|
</div>
|
|
17
17
|
|
|
18
18
|
## Features
|
|
@@ -115,13 +115,34 @@ class MyClient extends ApiClient {
|
|
|
115
115
|
}
|
|
116
116
|
|
|
117
117
|
try {
|
|
118
|
-
const api = MyClient()
|
|
118
|
+
const api = new MyClient()
|
|
119
119
|
const pet = await api.pet.getPetById(404)
|
|
120
120
|
} catch (e) {
|
|
121
121
|
console.log(e) // e is { code: "API_ERROR" }
|
|
122
122
|
}
|
|
123
123
|
```
|
|
124
124
|
|
|
125
|
+
### Base url resolving
|
|
126
|
+
|
|
127
|
+
You can modify how the endpoint url is created. By default [URL constructor](https://developer.mozilla.org/en-US/docs/Web/API/URL/URL) used to resolve endpoint url like: `new URL(path, baseUrl)` which has specific resolving [rules](https://developer.mozilla.org/en-US/docs/Web/API/URL_API/Resolving_relative_references). E.g.:
|
|
128
|
+
|
|
129
|
+
- `new URL("/v2/cats", "https://example.com/v1/") // -> https://example.com/v2/cats`
|
|
130
|
+
- `new URL("v2/cats", "https://example.com/v1/") // -> https://example.com/v1/v2/cats`
|
|
131
|
+
|
|
132
|
+
If you want to have custom endpoint url resolving rules, you can override `PrepareFetchUrl` method. For more details see [issue](https://github.com/vladkens/apigen-ts/issues/2).
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
class MyClient extends ApiClient {
|
|
136
|
+
PrepareFetchUrl(path: string) {
|
|
137
|
+
return new URL(`${this.Config.baseUrl}/${path}`.replace(/\/{2,}/g, "/"))
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const api = new MyClient({ baseUrl: "https://example.com/v1" })
|
|
142
|
+
// will call: https://example.com/v1/pet/ instead of https://example.com/pet/
|
|
143
|
+
const pet = await api.pet.getPetById(404)
|
|
144
|
+
```
|
|
145
|
+
|
|
125
146
|
### Node.js API
|
|
126
147
|
|
|
127
148
|
Create file like `apigen.mjs` with content:
|
|
@@ -147,6 +168,33 @@ await apigen({
|
|
|
147
168
|
|
|
148
169
|
Then run with: `node apigen.mjs`
|
|
149
170
|
|
|
171
|
+
## Usage with different backend frameworks
|
|
172
|
+
|
|
173
|
+
### FastAPI
|
|
174
|
+
|
|
175
|
+
By default `apigen-ts` generates noisy method names when used with FastAPI. This can be fixed by [custom resolving](https://fastapi.tiangolo.com/advanced/path-operation-advanced-configuration/#using-the-path-operation-function-name-as-the-operationid) for operations ids.
|
|
176
|
+
|
|
177
|
+
```py
|
|
178
|
+
from fastapi import FastAPI
|
|
179
|
+
from fastapi.routing import APIRoute
|
|
180
|
+
|
|
181
|
+
app = FastAPI()
|
|
182
|
+
|
|
183
|
+
# add your routes here
|
|
184
|
+
|
|
185
|
+
def update_operation_ids(app: FastAPI) -> None:
|
|
186
|
+
for route in app.routes:
|
|
187
|
+
if isinstance(route, APIRoute):
|
|
188
|
+
ns = route.tags[0] if route.tags else "general"
|
|
189
|
+
route.operation_id = f"{ns}_{route.name}".lower()
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
# this function should be after all routes added
|
|
193
|
+
update_operation_ids(app)
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
_Note: If you want FastAPI to be added as preset, open PR please._
|
|
197
|
+
|
|
150
198
|
## Compare
|
|
151
199
|
|
|
152
200
|
- [openapi-typescript-codegen](https://github.com/ferdikoomen/openapi-typescript-codegen) ([npm](https://www.npmjs.com/package/openapi-typescript-codegen)): no single file mode [#1263](https://github.com/ferdikoomen/openapi-typescript-codegen/issues/1263#issuecomment-1502890838)
|