@typia/utils 12.0.0-dev.20260310 → 12.0.0-dev.20260312

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.
Files changed (67) hide show
  1. package/lib/converters/LlmSchemaConverter.d.ts +0 -1
  2. package/lib/converters/LlmSchemaConverter.js +4 -31
  3. package/lib/converters/LlmSchemaConverter.js.map +1 -1
  4. package/lib/converters/LlmSchemaConverter.mjs +2 -32
  5. package/lib/converters/LlmSchemaConverter.mjs.map +1 -1
  6. package/lib/converters/OpenApiConverter.d.ts +29 -16
  7. package/lib/converters/OpenApiConverter.js +25 -15
  8. package/lib/converters/OpenApiConverter.js.map +1 -1
  9. package/lib/converters/OpenApiConverter.mjs +25 -15
  10. package/lib/converters/OpenApiConverter.mjs.map +1 -1
  11. package/lib/converters/internal/OpenApiV3Downgrader.js +38 -18
  12. package/lib/converters/internal/OpenApiV3Downgrader.js.map +1 -1
  13. package/lib/converters/internal/OpenApiV3Downgrader.mjs +50 -27
  14. package/lib/converters/internal/OpenApiV3Downgrader.mjs.map +1 -1
  15. package/lib/converters/internal/OpenApiV3Upgrader.js +36 -18
  16. package/lib/converters/internal/OpenApiV3Upgrader.js.map +1 -1
  17. package/lib/converters/internal/OpenApiV3Upgrader.mjs +50 -29
  18. package/lib/converters/internal/OpenApiV3Upgrader.mjs.map +1 -1
  19. package/lib/converters/internal/OpenApiV3_1Upgrader.js +38 -20
  20. package/lib/converters/internal/OpenApiV3_1Upgrader.js.map +1 -1
  21. package/lib/converters/internal/OpenApiV3_1Upgrader.mjs +51 -29
  22. package/lib/converters/internal/OpenApiV3_1Upgrader.mjs.map +1 -1
  23. package/lib/converters/internal/OpenApiV3_2Upgrader.d.ts +16 -0
  24. package/lib/converters/internal/OpenApiV3_2Upgrader.js +204 -0
  25. package/lib/converters/internal/OpenApiV3_2Upgrader.js.map +1 -0
  26. package/lib/converters/internal/OpenApiV3_2Upgrader.mjs +243 -0
  27. package/lib/converters/internal/OpenApiV3_2Upgrader.mjs.map +1 -0
  28. package/lib/converters/internal/SwaggerV2Downgrader.js +38 -18
  29. package/lib/converters/internal/SwaggerV2Downgrader.js.map +1 -1
  30. package/lib/converters/internal/SwaggerV2Downgrader.mjs +50 -27
  31. package/lib/converters/internal/SwaggerV2Downgrader.mjs.map +1 -1
  32. package/lib/converters/internal/SwaggerV2Upgrader.js +37 -19
  33. package/lib/converters/internal/SwaggerV2Upgrader.js.map +1 -1
  34. package/lib/converters/internal/SwaggerV2Upgrader.mjs +50 -29
  35. package/lib/converters/internal/SwaggerV2Upgrader.mjs.map +1 -1
  36. package/lib/http/HttpLlm.js +4 -5
  37. package/lib/http/HttpLlm.js.map +1 -1
  38. package/lib/http/HttpLlm.mjs +0 -1
  39. package/lib/http/HttpLlm.mjs.map +1 -1
  40. package/lib/http/internal/HttpLlmApplicationComposer.js +3 -4
  41. package/lib/http/internal/HttpLlmApplicationComposer.js.map +1 -1
  42. package/lib/http/internal/HttpLlmApplicationComposer.mjs +0 -1
  43. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  44. package/lib/http/internal/HttpMigrateApplicationComposer.js +1 -1
  45. package/lib/http/internal/HttpMigrateApplicationComposer.js.map +1 -1
  46. package/lib/http/internal/HttpMigrateApplicationComposer.mjs +1 -1
  47. package/lib/http/internal/HttpMigrateApplicationComposer.mjs.map +1 -1
  48. package/lib/http/internal/HttpMigrateRouteComposer.d.ts +1 -1
  49. package/lib/utils/LlmJson.d.ts +19 -1
  50. package/lib/utils/LlmJson.js +27 -0
  51. package/lib/utils/LlmJson.js.map +1 -1
  52. package/lib/utils/LlmJson.mjs +27 -0
  53. package/lib/utils/LlmJson.mjs.map +1 -1
  54. package/package.json +2 -2
  55. package/src/converters/LlmSchemaConverter.ts +1 -31
  56. package/src/converters/OpenApiConverter.ts +322 -285
  57. package/src/converters/internal/OpenApiV3Downgrader.ts +381 -355
  58. package/src/converters/internal/OpenApiV3Upgrader.ts +494 -470
  59. package/src/converters/internal/OpenApiV3_1Upgrader.ts +710 -685
  60. package/src/converters/internal/OpenApiV3_2Upgrader.ts +342 -0
  61. package/src/converters/internal/SwaggerV2Downgrader.ts +450 -424
  62. package/src/converters/internal/SwaggerV2Upgrader.ts +547 -523
  63. package/src/http/HttpLlm.ts +0 -1
  64. package/src/http/internal/HttpLlmApplicationComposer.ts +0 -1
  65. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  66. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  67. package/src/utils/LlmJson.ts +173 -141
@@ -1,505 +1,505 @@
1
- import { IHttpMigrateRoute, OpenApi } from "@typia/interface";
2
-
3
- import { NamingConvention } from "../../utils/NamingConvention";
4
- import { EndpointUtil } from "../../utils/internal/EndpointUtil";
5
- import { OpenApiTypeChecker } from "../../validators/OpenApiTypeChecker";
6
-
7
- export namespace HttpMigrateRouteComposer {
8
- export interface IProps {
9
- document: OpenApi.IDocument;
10
- method: "head" | "get" | "post" | "put" | "patch" | "delete";
11
- path: string;
12
- emendedPath: string;
13
- operation: OpenApi.IOperation;
14
- }
15
- export const compose = (props: IProps): IHttpMigrateRoute | string[] => {
16
- //----
17
- // REQUEST AND RESPONSE BODY
18
- //----
19
- const body: false | null | IHttpMigrateRoute.IBody = emplaceBodySchema(
20
- "request",
21
- )((schema) =>
22
- emplaceReference({
23
- document: props.document,
24
- name:
25
- EndpointUtil.pascal(`I/Api/${props.path}`) +
26
- "." +
27
- EndpointUtil.pascal(`${props.method}/Body`),
28
- schema,
29
- }),
30
- )(props.operation.requestBody);
31
- const success: false | null | IHttpMigrateRoute.ISuccess = (() => {
32
- const body = emplaceBodySchema("response")((schema) =>
33
- emplaceReference({
34
- document: props.document,
35
- name:
36
- EndpointUtil.pascal(`I/Api/${props.path}`) +
37
- "." +
38
- EndpointUtil.pascal(`${props.method}/Response`),
39
- schema,
40
- }),
41
- )(
42
- props.operation.responses?.["201"] ??
43
- props.operation.responses?.["200"] ??
44
- props.operation.responses?.default,
45
- );
46
- return body
47
- ? {
48
- ...body,
49
- status: props.operation.responses?.["201"]
50
- ? "201"
51
- : props.operation.responses?.["200"]
52
- ? "200"
53
- : "default",
54
- }
55
- : body;
56
- })();
57
-
58
- const failures: string[] = [];
59
- if (body === false)
60
- failures.push(
61
- `supports only "application/json", "application/x-www-form-urlencoded", "multipart/form-data" and "text/plain" content type in the request body.`,
62
- );
63
- if (success === false)
64
- failures.push(
65
- `supports only "application/json", "application/x-www-form-urlencoded" and "text/plain" content type in the response body.`,
66
- );
67
-
68
- //----
69
- // HEADERS AND QUERY
70
- //---
71
- const [headers, query] = ["header", "query"].map((type) => {
72
- // FIND TARGET PARAMETERS
73
- const parameters: OpenApi.IOperation.IParameter[] = (
74
- props.operation.parameters ?? []
75
- ).filter((p) => p.in === type);
76
- if (parameters.length === 0) return null;
77
-
78
- // CHECK PARAMETER TYPES -> TO BE OBJECT
79
- const objects = parameters
80
- .map((p) =>
81
- OpenApiTypeChecker.isObject(p.schema)
82
- ? p.schema
83
- : OpenApiTypeChecker.isReference(p.schema) &&
84
- OpenApiTypeChecker.isObject(
85
- props.document.components.schemas?.[
86
- p.schema.$ref.replace(`#/components/schemas/`, ``)
87
- ] ?? {},
88
- )
89
- ? p.schema
90
- : null!,
91
- )
92
- .filter((s) => !!s);
93
- const primitives = parameters.filter(
94
- (p) =>
95
- OpenApiTypeChecker.isBoolean(p.schema) ||
96
- OpenApiTypeChecker.isInteger(p.schema) ||
97
- OpenApiTypeChecker.isNumber(p.schema) ||
98
- OpenApiTypeChecker.isString(p.schema) ||
99
- OpenApiTypeChecker.isArray(p.schema) ||
100
- OpenApiTypeChecker.isTuple(p.schema),
101
- );
102
- const out = (elem: {
103
- schema: OpenApi.IJsonSchema;
104
- title?: string;
105
- description?: string;
106
- example?: any;
107
- examples?: Record<string, any>;
108
- }) =>
109
- ({
110
- ...elem,
111
- name: type,
112
- key: type,
113
- title: () => elem.title,
114
- description: () => elem.description,
115
- example: () => elem.example,
116
- examples: () => elem.examples,
117
- }) satisfies IHttpMigrateRoute.IHeaders;
118
-
119
- if (objects.length === 1 && primitives.length === 0)
120
- return out(parameters[0]!);
121
- else if (objects.length > 1) {
122
- failures.push(`${type} typed parameters must be only one object type`);
123
- return false;
124
- }
125
-
126
- // GATHER TO OBJECT TYPE
127
- const dto: OpenApi.IJsonSchema.IObject | null = objects[0]
128
- ? OpenApiTypeChecker.isObject(objects[0])
129
- ? objects[0]
130
- : ((props.document.components.schemas ?? {})[
131
- (objects[0] as OpenApi.IJsonSchema.IReference).$ref.replace(
132
- `#/components/schemas/`,
133
- ``,
134
- )
135
- ] as OpenApi.IJsonSchema.IObject)
136
- : null;
137
- const entire: OpenApi.IJsonSchema.IObject[] = [
138
- ...objects.map((o) =>
139
- OpenApiTypeChecker.isObject(o)
140
- ? o
141
- : (props.document.components.schemas?.[
142
- o.$ref.replace(`#/components/schemas/`, ``)
143
- ]! as OpenApi.IJsonSchema.IObject),
144
- ),
145
- {
146
- type: "object",
147
- properties: Object.fromEntries([
148
- ...primitives.map((p) => [
149
- p.name,
150
- {
151
- ...p.schema,
152
- description: p.schema.description ?? p.description,
153
- },
154
- ]),
155
- ...(dto ? Object.entries(dto.properties ?? {}) : []),
156
- ]),
157
- required: [
158
- ...new Set([
159
- ...primitives.filter((p) => p.required).map((p) => p.name!),
160
- ...(dto?.required ?? []),
161
- ]),
162
- ],
163
- },
164
- ];
165
- return parameters.length === 0
166
- ? null
167
- : out({
168
- schema: emplaceReference({
169
- document: props.document,
170
- name:
171
- EndpointUtil.pascal(`I/Api/${props.path}`) +
172
- "." +
173
- EndpointUtil.pascal(`${props.method}/${type}`),
174
- schema: {
175
- type: "object",
176
- properties: Object.fromEntries([
177
- ...new Map<string, OpenApi.IJsonSchema>(
178
- entire
179
- .map((o) =>
180
- Object.entries(o.properties ?? {}).map(
181
- ([name, schema]) =>
182
- [
183
- name,
184
- {
185
- ...schema,
186
- description:
187
- schema.description ?? schema.description,
188
- } as OpenApi.IJsonSchema,
189
- ] as const,
190
- ),
191
- )
192
- .flat(),
193
- ),
194
- ]),
195
- required: [
196
- ...new Set(entire.map((o) => o.required ?? []).flat()),
197
- ],
198
- } satisfies OpenApi.IJsonSchema.IObject,
199
- }),
200
- });
201
- });
202
-
203
- //----
204
- // PATH PARAMETERS
205
- //----
206
- const parameterNames: string[] = EndpointUtil.splitWithNormalization(
207
- props.emendedPath,
208
- )
209
- .filter((str) => str[0] === ":")
210
- .map((str) => str.substring(1));
211
- const pathParameters: OpenApi.IOperation.IParameter[] = (
212
- props.operation.parameters ?? []
213
- ).filter((p) => p.in === "path");
214
- if (parameterNames.length !== pathParameters.length)
215
- if (
216
- pathParameters.length < parameterNames.length &&
217
- pathParameters.every(
218
- (p) => p.name !== undefined && parameterNames.includes(p.name),
219
- )
220
- ) {
221
- for (const name of parameterNames)
222
- if (pathParameters.find((p) => p.name === name) === undefined)
223
- pathParameters.push({
224
- name,
225
- in: "path",
226
- schema: { type: "string" },
227
- });
228
- pathParameters.sort(
229
- (a, b) =>
230
- parameterNames.indexOf(a.name!) - parameterNames.indexOf(b.name!),
231
- );
232
- props.operation.parameters = [
233
- ...pathParameters,
234
- ...(props.operation.parameters ?? []).filter((p) => p.in !== "path"),
235
- ];
236
- } else
237
- failures.push(
238
- "number of path parameters are not matched with its full path.",
239
- );
240
- if (failures.length) return failures;
241
-
242
- const parameters: IHttpMigrateRoute.IParameter[] = (
243
- props.operation.parameters ?? []
244
- )
245
- .filter((p) => p.in === "path")
246
- .map((p, i) => ({
247
- // FILL KEY NAME IF NOT EXISTS
248
- name: parameterNames[i]!,
249
- key: (() => {
250
- let key: string = EndpointUtil.normalize(parameterNames[i]!);
251
- if (NamingConvention.variable(key)) return key;
252
- while (true) {
253
- key = "_" + key;
254
- if (!parameterNames.some((s) => s === key)) return key;
255
- }
256
- })(),
257
- schema: p.schema,
258
- parameter: () => p,
259
- }));
260
- return {
261
- method: props.method,
262
- path: props.path,
263
- emendedPath: props.emendedPath,
264
- accessor: ["@lazy"],
265
- parameters: (props.operation.parameters ?? [])
266
- .filter((p) => p.in === "path")
267
- .map((p, i) => ({
268
- // FILL KEY NAME IF NOT EXISTS
269
- name: parameterNames[i]!,
270
- key: (() => {
271
- let key: string = EndpointUtil.normalize(parameterNames[i]!);
272
- if (NamingConvention.variable(key)) return key;
273
- while (true) {
274
- key = "_" + key;
275
- if (!parameterNames.some((s) => s === key)) return key;
276
- }
277
- })(),
278
- schema: p.schema,
279
- parameter: () => p,
280
- })),
281
- headers: headers || null,
282
- query: query || null,
283
- body: body || null,
284
- success: success || null,
285
- exceptions: Object.fromEntries(
286
- Object.entries(props.operation.responses ?? {})
287
- .filter(
288
- ([key]) => key !== "200" && key !== "201" && key !== "default",
289
- )
290
- .map(([status, response]) => [
291
- status,
292
- {
293
- schema: (response.content?.["application/json"]?.schema ??
294
- {}) satisfies OpenApi.IJsonSchema,
295
- response: () => response,
296
- media: () =>
297
- (response.content?.["application/json"] ??
298
- {}) satisfies OpenApi.IJsonSchema,
299
- } satisfies IHttpMigrateRoute.IException,
300
- ]),
301
- ),
302
- comment: () =>
303
- writeRouteComment({
304
- operation: props.operation,
305
- parameters,
306
- query: query || null,
307
- body: body || null,
308
- }),
309
- operation: () => props.operation,
310
- } satisfies IHttpMigrateRoute as IHttpMigrateRoute;
311
- };
312
-
313
- const writeRouteComment = (props: {
314
- operation: OpenApi.IOperation;
315
- parameters: IHttpMigrateRoute.IParameter[];
316
- query: IHttpMigrateRoute.IQuery | null;
317
- body: IHttpMigrateRoute.IBody | null;
318
- }): string => {
319
- // write basic description combining with summary
320
- let description: string = props.operation.description ?? "";
321
- if (!!props.operation.summary?.length) {
322
- const summary: string = props.operation.summary.endsWith(".")
323
- ? props.operation.summary
324
- : props.operation.summary + ".";
325
- if (
326
- !!description.length &&
327
- !description.startsWith(props.operation.summary)
328
- )
329
- description = `${summary}\n\n${description}`;
330
- }
331
- description = description
332
- .split("\n")
333
- .map((s) => s.trim())
334
- .join("\n");
335
-
336
- //----
337
- // compose jsdoc comment tags
338
- //----
339
- const commentTags: string[] = [];
340
- const add = (text: string) => {
341
- if (commentTags.every((line) => line !== text)) commentTags.push(text);
342
- };
343
-
344
- // parameters
345
- add("@param connection");
346
- for (const p of props.parameters ?? []) {
347
- const param = p.parameter();
348
- if (param.description) {
349
- const text: string = param.description!;
350
- add(`@param ${p.name} ${writeIndented(text, p.name.length + 8)}`);
351
- }
352
- }
353
- if (props.body?.description()?.length)
354
- add(`@param body ${writeIndented(props.body.description()!, 12)}`);
355
-
356
- // security
357
- for (const security of props.operation.security ?? [])
358
- for (const [name, scopes] of Object.entries(security))
359
- add(`@security ${[name, ...scopes].join("")}`);
360
-
361
- // categorizing tags
362
- if (props.operation.tags)
363
- props.operation.tags.forEach((name) => add(`@tag ${name}`));
364
-
365
- // deprecated
366
- if (props.operation.deprecated) add("@deprecated");
367
-
368
- // plugin properties
369
- for (const [key, value] of Object.entries(props.operation)) {
370
- if (key.startsWith("x-") === false) continue;
371
- else if (
372
- value !== null &&
373
- typeof value !== "boolean" &&
374
- typeof value !== "number" &&
375
- typeof value !== "string"
376
- )
377
- continue;
378
- add(`@${key} ${value}`);
379
- }
380
-
381
- // finalize description
382
- description = description.length
383
- ? commentTags.length
384
- ? `${description}\n\n${commentTags.join("\n")}`
385
- : description
386
- : commentTags.join("\n");
387
- description = description.split("*/").join("*\\/");
388
- return description;
389
- };
390
-
391
- const writeIndented = (text: string, spaces: number): string =>
392
- text
393
- .split("\n")
394
- .map((s) => s.trim())
395
- .map((s, i) => (i === 0 ? s : `${" ".repeat(spaces)}${s}`))
396
- .join("\n");
397
-
398
- const emplaceBodySchema =
399
- (from: "request" | "response") =>
400
- (
401
- emplacer: (schema: OpenApi.IJsonSchema) => OpenApi.IJsonSchema.IReference,
402
- ) =>
403
- (meta?: {
404
- description?: string;
405
- content?: Partial<Record<string, OpenApi.IOperation.IMediaType>>; // ISwaggerRouteBodyContent;
406
- "x-nestia-encrypted"?: boolean;
407
- }): false | null | IHttpMigrateRoute.IBody => {
408
- if (!meta?.content) return null;
409
-
410
- const entries: [string, OpenApi.IOperation.IMediaType][] = Object.entries(
411
- meta.content,
412
- ).filter(([_, v]) => !!v) as [string, OpenApi.IOperation.IMediaType][];
413
- const json = entries.find((e) =>
414
- meta["x-nestia-encrypted"] === true
415
- ? e[0].includes("text/plain") || e[0].includes("application/json")
416
- : e[0].includes("application/json") || e[0].includes("*/*"),
417
- );
418
- if (json) {
419
- const { schema } = json[1];
420
- return schema
421
- ? {
422
- type: "application/json",
423
- name: "body",
424
- key: "body",
425
- schema: isNotObjectLiteral(schema) ? schema : emplacer(schema),
426
- description: () => meta.description,
427
- media: () => json[1],
428
- "x-nestia-encrypted": meta["x-nestia-encrypted"],
429
- }
430
- : null;
431
- }
432
-
433
- const query = entries.find((e) =>
434
- e[0].includes("application/x-www-form-urlencoded"),
435
- );
436
- if (query) {
437
- const { schema } = query[1];
438
- return schema
439
- ? {
440
- type: "application/x-www-form-urlencoded",
441
- name: "body",
442
- key: "body",
443
- schema: isNotObjectLiteral(schema) ? schema : emplacer(schema),
444
- description: () => meta.description,
445
- media: () => query[1],
446
- }
447
- : null;
448
- }
449
-
450
- const text = entries.find((e) => e[0].includes("text/plain"));
451
- if (text)
452
- return {
453
- type: "text/plain",
454
- name: "body",
455
- key: "body",
456
- schema: { type: "string" },
457
- description: () => meta.description,
458
- media: () => text[1],
459
- };
460
-
461
- if (from === "request") {
462
- const multipart = entries.find((e) =>
463
- e[0].includes("multipart/form-data"),
464
- );
465
- if (multipart) {
466
- const { schema } = multipart[1];
467
- return {
468
- type: "multipart/form-data",
469
- name: "body",
470
- key: "body",
471
- schema: schema
472
- ? isNotObjectLiteral(schema)
473
- ? schema
474
- : emplacer(schema)
475
- : {},
476
- description: () => meta.description,
477
- media: () => multipart[1],
478
- };
479
- }
480
- }
481
- return false;
482
- };
483
-
484
- const emplaceReference = (props: {
485
- document: OpenApi.IDocument;
486
- name: string;
487
- schema: OpenApi.IJsonSchema;
488
- }): OpenApi.IJsonSchema.IReference => {
489
- props.document.components.schemas ??= {};
490
- props.document.components.schemas[props.name] = props.schema;
491
- return {
492
- $ref: `#/components/schemas/${props.name}`,
493
- } satisfies OpenApi.IJsonSchema.IReference;
494
- };
495
-
496
- const isNotObjectLiteral = (schema: OpenApi.IJsonSchema): boolean =>
497
- OpenApiTypeChecker.isReference(schema) ||
498
- OpenApiTypeChecker.isBoolean(schema) ||
499
- OpenApiTypeChecker.isNumber(schema) ||
500
- OpenApiTypeChecker.isString(schema) ||
501
- OpenApiTypeChecker.isUnknown(schema) ||
502
- (OpenApiTypeChecker.isOneOf(schema) &&
503
- schema.oneOf.every(isNotObjectLiteral)) ||
504
- (OpenApiTypeChecker.isArray(schema) && isNotObjectLiteral(schema.items));
505
- }
1
+ import { IHttpMigrateRoute, OpenApi } from "@typia/interface";
2
+
3
+ import { NamingConvention } from "../../utils/NamingConvention";
4
+ import { EndpointUtil } from "../../utils/internal/EndpointUtil";
5
+ import { OpenApiTypeChecker } from "../../validators/OpenApiTypeChecker";
6
+
7
+ export namespace HttpMigrateRouteComposer {
8
+ export interface IProps {
9
+ document: OpenApi.IDocument;
10
+ method: "head" | "get" | "post" | "put" | "patch" | "delete" | "query";
11
+ path: string;
12
+ emendedPath: string;
13
+ operation: OpenApi.IOperation;
14
+ }
15
+ export const compose = (props: IProps): IHttpMigrateRoute | string[] => {
16
+ //----
17
+ // REQUEST AND RESPONSE BODY
18
+ //----
19
+ const body: false | null | IHttpMigrateRoute.IBody = emplaceBodySchema(
20
+ "request",
21
+ )((schema) =>
22
+ emplaceReference({
23
+ document: props.document,
24
+ name:
25
+ EndpointUtil.pascal(`I/Api/${props.path}`) +
26
+ "." +
27
+ EndpointUtil.pascal(`${props.method}/Body`),
28
+ schema,
29
+ }),
30
+ )(props.operation.requestBody);
31
+ const success: false | null | IHttpMigrateRoute.ISuccess = (() => {
32
+ const body = emplaceBodySchema("response")((schema) =>
33
+ emplaceReference({
34
+ document: props.document,
35
+ name:
36
+ EndpointUtil.pascal(`I/Api/${props.path}`) +
37
+ "." +
38
+ EndpointUtil.pascal(`${props.method}/Response`),
39
+ schema,
40
+ }),
41
+ )(
42
+ props.operation.responses?.["201"] ??
43
+ props.operation.responses?.["200"] ??
44
+ props.operation.responses?.default,
45
+ );
46
+ return body
47
+ ? {
48
+ ...body,
49
+ status: props.operation.responses?.["201"]
50
+ ? "201"
51
+ : props.operation.responses?.["200"]
52
+ ? "200"
53
+ : "default",
54
+ }
55
+ : body;
56
+ })();
57
+
58
+ const failures: string[] = [];
59
+ if (body === false)
60
+ failures.push(
61
+ `supports only "application/json", "application/x-www-form-urlencoded", "multipart/form-data" and "text/plain" content type in the request body.`,
62
+ );
63
+ if (success === false)
64
+ failures.push(
65
+ `supports only "application/json", "application/x-www-form-urlencoded" and "text/plain" content type in the response body.`,
66
+ );
67
+
68
+ //----
69
+ // HEADERS AND QUERY
70
+ //---
71
+ const [headers, query] = ["header", "query"].map((type) => {
72
+ // FIND TARGET PARAMETERS
73
+ const parameters: OpenApi.IOperation.IParameter[] = (
74
+ props.operation.parameters ?? []
75
+ ).filter((p) => p.in === type);
76
+ if (parameters.length === 0) return null;
77
+
78
+ // CHECK PARAMETER TYPES -> TO BE OBJECT
79
+ const objects = parameters
80
+ .map((p) =>
81
+ OpenApiTypeChecker.isObject(p.schema)
82
+ ? p.schema
83
+ : OpenApiTypeChecker.isReference(p.schema) &&
84
+ OpenApiTypeChecker.isObject(
85
+ props.document.components.schemas?.[
86
+ p.schema.$ref.replace(`#/components/schemas/`, ``)
87
+ ] ?? {},
88
+ )
89
+ ? p.schema
90
+ : null!,
91
+ )
92
+ .filter((s) => !!s);
93
+ const primitives = parameters.filter(
94
+ (p) =>
95
+ OpenApiTypeChecker.isBoolean(p.schema) ||
96
+ OpenApiTypeChecker.isInteger(p.schema) ||
97
+ OpenApiTypeChecker.isNumber(p.schema) ||
98
+ OpenApiTypeChecker.isString(p.schema) ||
99
+ OpenApiTypeChecker.isArray(p.schema) ||
100
+ OpenApiTypeChecker.isTuple(p.schema),
101
+ );
102
+ const out = (elem: {
103
+ schema: OpenApi.IJsonSchema;
104
+ title?: string;
105
+ description?: string;
106
+ example?: any;
107
+ examples?: Record<string, any>;
108
+ }) =>
109
+ ({
110
+ ...elem,
111
+ name: type,
112
+ key: type,
113
+ title: () => elem.title,
114
+ description: () => elem.description,
115
+ example: () => elem.example,
116
+ examples: () => elem.examples,
117
+ }) satisfies IHttpMigrateRoute.IHeaders;
118
+
119
+ if (objects.length === 1 && primitives.length === 0)
120
+ return out(parameters[0]!);
121
+ else if (objects.length > 1) {
122
+ failures.push(`${type} typed parameters must be only one object type`);
123
+ return false;
124
+ }
125
+
126
+ // GATHER TO OBJECT TYPE
127
+ const dto: OpenApi.IJsonSchema.IObject | null = objects[0]
128
+ ? OpenApiTypeChecker.isObject(objects[0])
129
+ ? objects[0]
130
+ : ((props.document.components.schemas ?? {})[
131
+ (objects[0] as OpenApi.IJsonSchema.IReference).$ref.replace(
132
+ `#/components/schemas/`,
133
+ ``,
134
+ )
135
+ ] as OpenApi.IJsonSchema.IObject)
136
+ : null;
137
+ const entire: OpenApi.IJsonSchema.IObject[] = [
138
+ ...objects.map((o) =>
139
+ OpenApiTypeChecker.isObject(o)
140
+ ? o
141
+ : (props.document.components.schemas?.[
142
+ o.$ref.replace(`#/components/schemas/`, ``)
143
+ ]! as OpenApi.IJsonSchema.IObject),
144
+ ),
145
+ {
146
+ type: "object",
147
+ properties: Object.fromEntries([
148
+ ...primitives.map((p) => [
149
+ p.name,
150
+ {
151
+ ...p.schema,
152
+ description: p.schema.description ?? p.description,
153
+ },
154
+ ]),
155
+ ...(dto ? Object.entries(dto.properties ?? {}) : []),
156
+ ]),
157
+ required: [
158
+ ...new Set([
159
+ ...primitives.filter((p) => p.required).map((p) => p.name!),
160
+ ...(dto?.required ?? []),
161
+ ]),
162
+ ],
163
+ },
164
+ ];
165
+ return parameters.length === 0
166
+ ? null
167
+ : out({
168
+ schema: emplaceReference({
169
+ document: props.document,
170
+ name:
171
+ EndpointUtil.pascal(`I/Api/${props.path}`) +
172
+ "." +
173
+ EndpointUtil.pascal(`${props.method}/${type}`),
174
+ schema: {
175
+ type: "object",
176
+ properties: Object.fromEntries([
177
+ ...new Map<string, OpenApi.IJsonSchema>(
178
+ entire
179
+ .map((o) =>
180
+ Object.entries(o.properties ?? {}).map(
181
+ ([name, schema]) =>
182
+ [
183
+ name,
184
+ {
185
+ ...schema,
186
+ description:
187
+ schema.description ?? schema.description,
188
+ } as OpenApi.IJsonSchema,
189
+ ] as const,
190
+ ),
191
+ )
192
+ .flat(),
193
+ ),
194
+ ]),
195
+ required: [
196
+ ...new Set(entire.map((o) => o.required ?? []).flat()),
197
+ ],
198
+ } satisfies OpenApi.IJsonSchema.IObject,
199
+ }),
200
+ });
201
+ });
202
+
203
+ //----
204
+ // PATH PARAMETERS
205
+ //----
206
+ const parameterNames: string[] = EndpointUtil.splitWithNormalization(
207
+ props.emendedPath,
208
+ )
209
+ .filter((str) => str[0] === ":")
210
+ .map((str) => str.substring(1));
211
+ const pathParameters: OpenApi.IOperation.IParameter[] = (
212
+ props.operation.parameters ?? []
213
+ ).filter((p) => p.in === "path");
214
+ if (parameterNames.length !== pathParameters.length)
215
+ if (
216
+ pathParameters.length < parameterNames.length &&
217
+ pathParameters.every(
218
+ (p) => p.name !== undefined && parameterNames.includes(p.name),
219
+ )
220
+ ) {
221
+ for (const name of parameterNames)
222
+ if (pathParameters.find((p) => p.name === name) === undefined)
223
+ pathParameters.push({
224
+ name,
225
+ in: "path",
226
+ schema: { type: "string" },
227
+ });
228
+ pathParameters.sort(
229
+ (a, b) =>
230
+ parameterNames.indexOf(a.name!) - parameterNames.indexOf(b.name!),
231
+ );
232
+ props.operation.parameters = [
233
+ ...pathParameters,
234
+ ...(props.operation.parameters ?? []).filter((p) => p.in !== "path"),
235
+ ];
236
+ } else
237
+ failures.push(
238
+ "number of path parameters are not matched with its full path.",
239
+ );
240
+ if (failures.length) return failures;
241
+
242
+ const parameters: IHttpMigrateRoute.IParameter[] = (
243
+ props.operation.parameters ?? []
244
+ )
245
+ .filter((p) => p.in === "path")
246
+ .map((p, i) => ({
247
+ // FILL KEY NAME IF NOT EXISTS
248
+ name: parameterNames[i]!,
249
+ key: (() => {
250
+ let key: string = EndpointUtil.normalize(parameterNames[i]!);
251
+ if (NamingConvention.variable(key)) return key;
252
+ while (true) {
253
+ key = "_" + key;
254
+ if (!parameterNames.some((s) => s === key)) return key;
255
+ }
256
+ })(),
257
+ schema: p.schema,
258
+ parameter: () => p,
259
+ }));
260
+ return {
261
+ method: props.method,
262
+ path: props.path,
263
+ emendedPath: props.emendedPath,
264
+ accessor: ["@lazy"],
265
+ parameters: (props.operation.parameters ?? [])
266
+ .filter((p) => p.in === "path")
267
+ .map((p, i) => ({
268
+ // FILL KEY NAME IF NOT EXISTS
269
+ name: parameterNames[i]!,
270
+ key: (() => {
271
+ let key: string = EndpointUtil.normalize(parameterNames[i]!);
272
+ if (NamingConvention.variable(key)) return key;
273
+ while (true) {
274
+ key = "_" + key;
275
+ if (!parameterNames.some((s) => s === key)) return key;
276
+ }
277
+ })(),
278
+ schema: p.schema,
279
+ parameter: () => p,
280
+ })),
281
+ headers: headers || null,
282
+ query: query || null,
283
+ body: body || null,
284
+ success: success || null,
285
+ exceptions: Object.fromEntries(
286
+ Object.entries(props.operation.responses ?? {})
287
+ .filter(
288
+ ([key]) => key !== "200" && key !== "201" && key !== "default",
289
+ )
290
+ .map(([status, response]) => [
291
+ status,
292
+ {
293
+ schema: (response.content?.["application/json"]?.schema ??
294
+ {}) satisfies OpenApi.IJsonSchema,
295
+ response: () => response,
296
+ media: () =>
297
+ (response.content?.["application/json"] ??
298
+ {}) satisfies OpenApi.IJsonSchema,
299
+ } satisfies IHttpMigrateRoute.IException,
300
+ ]),
301
+ ),
302
+ comment: () =>
303
+ writeRouteComment({
304
+ operation: props.operation,
305
+ parameters,
306
+ query: query || null,
307
+ body: body || null,
308
+ }),
309
+ operation: () => props.operation,
310
+ } satisfies IHttpMigrateRoute as IHttpMigrateRoute;
311
+ };
312
+
313
+ const writeRouteComment = (props: {
314
+ operation: OpenApi.IOperation;
315
+ parameters: IHttpMigrateRoute.IParameter[];
316
+ query: IHttpMigrateRoute.IQuery | null;
317
+ body: IHttpMigrateRoute.IBody | null;
318
+ }): string => {
319
+ // write basic description combining with summary
320
+ let description: string = props.operation.description ?? "";
321
+ if (!!props.operation.summary?.length) {
322
+ const summary: string = props.operation.summary.endsWith(".")
323
+ ? props.operation.summary
324
+ : props.operation.summary + ".";
325
+ if (
326
+ !!description.length &&
327
+ !description.startsWith(props.operation.summary)
328
+ )
329
+ description = `${summary}\n\n${description}`;
330
+ }
331
+ description = description
332
+ .split("\n")
333
+ .map((s) => s.trim())
334
+ .join("\n");
335
+
336
+ //----
337
+ // compose jsdoc comment tags
338
+ //----
339
+ const commentTags: string[] = [];
340
+ const add = (text: string) => {
341
+ if (commentTags.every((line) => line !== text)) commentTags.push(text);
342
+ };
343
+
344
+ // parameters
345
+ add("@param connection");
346
+ for (const p of props.parameters ?? []) {
347
+ const param = p.parameter();
348
+ if (param.description) {
349
+ const text: string = param.description!;
350
+ add(`@param ${p.name} ${writeIndented(text, p.name.length + 8)}`);
351
+ }
352
+ }
353
+ if (props.body?.description()?.length)
354
+ add(`@param body ${writeIndented(props.body.description()!, 12)}`);
355
+
356
+ // security
357
+ for (const security of props.operation.security ?? [])
358
+ for (const [name, scopes] of Object.entries(security))
359
+ add(`@security ${[name, ...scopes].join("")}`);
360
+
361
+ // categorizing tags
362
+ if (props.operation.tags)
363
+ props.operation.tags.forEach((name) => add(`@tag ${name}`));
364
+
365
+ // deprecated
366
+ if (props.operation.deprecated) add("@deprecated");
367
+
368
+ // plugin properties
369
+ for (const [key, value] of Object.entries(props.operation)) {
370
+ if (key.startsWith("x-") === false) continue;
371
+ else if (
372
+ value !== null &&
373
+ typeof value !== "boolean" &&
374
+ typeof value !== "number" &&
375
+ typeof value !== "string"
376
+ )
377
+ continue;
378
+ add(`@${key} ${value}`);
379
+ }
380
+
381
+ // finalize description
382
+ description = description.length
383
+ ? commentTags.length
384
+ ? `${description}\n\n${commentTags.join("\n")}`
385
+ : description
386
+ : commentTags.join("\n");
387
+ description = description.split("*/").join("*\\/");
388
+ return description;
389
+ };
390
+
391
+ const writeIndented = (text: string, spaces: number): string =>
392
+ text
393
+ .split("\n")
394
+ .map((s) => s.trim())
395
+ .map((s, i) => (i === 0 ? s : `${" ".repeat(spaces)}${s}`))
396
+ .join("\n");
397
+
398
+ const emplaceBodySchema =
399
+ (from: "request" | "response") =>
400
+ (
401
+ emplacer: (schema: OpenApi.IJsonSchema) => OpenApi.IJsonSchema.IReference,
402
+ ) =>
403
+ (meta?: {
404
+ description?: string;
405
+ content?: Partial<Record<string, OpenApi.IOperation.IMediaType>>; // ISwaggerRouteBodyContent;
406
+ "x-nestia-encrypted"?: boolean;
407
+ }): false | null | IHttpMigrateRoute.IBody => {
408
+ if (!meta?.content) return null;
409
+
410
+ const entries: [string, OpenApi.IOperation.IMediaType][] = Object.entries(
411
+ meta.content,
412
+ ).filter(([_, v]) => !!v) as [string, OpenApi.IOperation.IMediaType][];
413
+ const json = entries.find((e) =>
414
+ meta["x-nestia-encrypted"] === true
415
+ ? e[0].includes("text/plain") || e[0].includes("application/json")
416
+ : e[0].includes("application/json") || e[0].includes("*/*"),
417
+ );
418
+ if (json) {
419
+ const { schema } = json[1];
420
+ return schema
421
+ ? {
422
+ type: "application/json",
423
+ name: "body",
424
+ key: "body",
425
+ schema: isNotObjectLiteral(schema) ? schema : emplacer(schema),
426
+ description: () => meta.description,
427
+ media: () => json[1],
428
+ "x-nestia-encrypted": meta["x-nestia-encrypted"],
429
+ }
430
+ : null;
431
+ }
432
+
433
+ const query = entries.find((e) =>
434
+ e[0].includes("application/x-www-form-urlencoded"),
435
+ );
436
+ if (query) {
437
+ const { schema } = query[1];
438
+ return schema
439
+ ? {
440
+ type: "application/x-www-form-urlencoded",
441
+ name: "body",
442
+ key: "body",
443
+ schema: isNotObjectLiteral(schema) ? schema : emplacer(schema),
444
+ description: () => meta.description,
445
+ media: () => query[1],
446
+ }
447
+ : null;
448
+ }
449
+
450
+ const text = entries.find((e) => e[0].includes("text/plain"));
451
+ if (text)
452
+ return {
453
+ type: "text/plain",
454
+ name: "body",
455
+ key: "body",
456
+ schema: { type: "string" },
457
+ description: () => meta.description,
458
+ media: () => text[1],
459
+ };
460
+
461
+ if (from === "request") {
462
+ const multipart = entries.find((e) =>
463
+ e[0].includes("multipart/form-data"),
464
+ );
465
+ if (multipart) {
466
+ const { schema } = multipart[1];
467
+ return {
468
+ type: "multipart/form-data",
469
+ name: "body",
470
+ key: "body",
471
+ schema: schema
472
+ ? isNotObjectLiteral(schema)
473
+ ? schema
474
+ : emplacer(schema)
475
+ : {},
476
+ description: () => meta.description,
477
+ media: () => multipart[1],
478
+ };
479
+ }
480
+ }
481
+ return false;
482
+ };
483
+
484
+ const emplaceReference = (props: {
485
+ document: OpenApi.IDocument;
486
+ name: string;
487
+ schema: OpenApi.IJsonSchema;
488
+ }): OpenApi.IJsonSchema.IReference => {
489
+ props.document.components.schemas ??= {};
490
+ props.document.components.schemas[props.name] = props.schema;
491
+ return {
492
+ $ref: `#/components/schemas/${props.name}`,
493
+ } satisfies OpenApi.IJsonSchema.IReference;
494
+ };
495
+
496
+ const isNotObjectLiteral = (schema: OpenApi.IJsonSchema): boolean =>
497
+ OpenApiTypeChecker.isReference(schema) ||
498
+ OpenApiTypeChecker.isBoolean(schema) ||
499
+ OpenApiTypeChecker.isNumber(schema) ||
500
+ OpenApiTypeChecker.isString(schema) ||
501
+ OpenApiTypeChecker.isUnknown(schema) ||
502
+ (OpenApiTypeChecker.isOneOf(schema) &&
503
+ schema.oneOf.every(isNotObjectLiteral)) ||
504
+ (OpenApiTypeChecker.isArray(schema) && isNotObjectLiteral(schema.items));
505
+ }