@typia/utils 12.0.0-dev.20260307-2 → 12.0.0-dev.20260310

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 (103) hide show
  1. package/README.md +2 -2
  2. package/lib/http/internal/HttpLlmApplicationComposer.js +1 -0
  3. package/lib/http/internal/HttpLlmApplicationComposer.js.map +1 -1
  4. package/lib/http/internal/HttpLlmApplicationComposer.mjs +1 -0
  5. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  6. package/lib/utils/LlmJson.d.ts +3 -3
  7. package/lib/utils/LlmJson.js +2 -2
  8. package/lib/utils/LlmJson.js.map +1 -1
  9. package/lib/utils/LlmJson.mjs +2 -2
  10. package/lib/utils/LlmJson.mjs.map +1 -1
  11. package/lib/utils/internal/coerceLlmArguments.js +17 -1
  12. package/lib/utils/internal/coerceLlmArguments.js.map +1 -1
  13. package/lib/utils/internal/coerceLlmArguments.mjs +17 -1
  14. package/lib/utils/internal/coerceLlmArguments.mjs.map +1 -1
  15. package/lib/utils/internal/parseLenientJson.js +236 -96
  16. package/lib/utils/internal/parseLenientJson.js.map +1 -1
  17. package/lib/utils/internal/parseLenientJson.mjs +236 -96
  18. package/lib/utils/internal/parseLenientJson.mjs.map +1 -1
  19. package/lib/utils/internal/stringifyValidationFailure.js +17 -15
  20. package/lib/utils/internal/stringifyValidationFailure.js.map +1 -1
  21. package/lib/utils/internal/stringifyValidationFailure.mjs +17 -15
  22. package/lib/utils/internal/stringifyValidationFailure.mjs.map +1 -1
  23. package/package.json +2 -2
  24. package/src/converters/LlmSchemaConverter.ts +647 -647
  25. package/src/converters/OpenApiConverter.ts +285 -285
  26. package/src/converters/index.ts +5 -5
  27. package/src/converters/internal/LlmDescriptionInverter.ts +178 -178
  28. package/src/converters/internal/LlmParametersComposer.ts +52 -52
  29. package/src/converters/internal/OpenApiConstraintShifter.ts +154 -154
  30. package/src/converters/internal/OpenApiExclusiveEmender.ts +46 -46
  31. package/src/converters/internal/OpenApiV3Downgrader.ts +355 -355
  32. package/src/converters/internal/OpenApiV3Upgrader.ts +470 -470
  33. package/src/converters/internal/OpenApiV3_1Upgrader.ts +685 -685
  34. package/src/converters/internal/SwaggerV2Downgrader.ts +424 -424
  35. package/src/converters/internal/SwaggerV2Upgrader.ts +523 -523
  36. package/src/http/HttpError.ts +107 -107
  37. package/src/http/HttpLlm.ts +167 -167
  38. package/src/http/HttpMigration.ts +92 -92
  39. package/src/http/index.ts +3 -3
  40. package/src/http/internal/HttpLlmApplicationComposer.ts +361 -360
  41. package/src/http/internal/HttpLlmFunctionFetcher.ts +37 -37
  42. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  43. package/src/http/internal/HttpMigrateRouteAccessor.ts +135 -135
  44. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  45. package/src/http/internal/HttpMigrateRouteFetcher.ts +203 -203
  46. package/src/index.ts +4 -4
  47. package/src/utils/ArrayUtil.ts +42 -42
  48. package/src/utils/LlmJson.ts +141 -141
  49. package/src/utils/MapUtil.ts +15 -15
  50. package/src/utils/NamingConvention.ts +205 -205
  51. package/src/utils/Singleton.ts +17 -17
  52. package/src/utils/StringUtil.ts +14 -14
  53. package/src/utils/dedent.ts +57 -57
  54. package/src/utils/index.ts +8 -8
  55. package/src/utils/internal/EndpointUtil.ts +44 -44
  56. package/src/utils/internal/JsonDescriptor.ts +70 -70
  57. package/src/utils/internal/OpenApiTypeCheckerBase.ts +822 -822
  58. package/src/utils/internal/coerceLlmArguments.ts +314 -297
  59. package/src/utils/internal/parseLenientJson.ts +894 -731
  60. package/src/utils/internal/stringifyValidationFailure.ts +415 -411
  61. package/src/validators/LlmTypeChecker.ts +402 -402
  62. package/src/validators/OpenApiTypeChecker.ts +297 -297
  63. package/src/validators/OpenApiV3TypeChecker.ts +70 -70
  64. package/src/validators/OpenApiV3_1TypeChecker.ts +86 -86
  65. package/src/validators/OpenApiValidator.ts +94 -94
  66. package/src/validators/SwaggerV2TypeChecker.ts +71 -71
  67. package/src/validators/functional/_isBigintString.ts +8 -8
  68. package/src/validators/functional/_isFormatByte.ts +7 -7
  69. package/src/validators/functional/_isFormatDate.ts +3 -3
  70. package/src/validators/functional/_isFormatDateTime.ts +4 -4
  71. package/src/validators/functional/_isFormatDuration.ts +4 -4
  72. package/src/validators/functional/_isFormatEmail.ts +4 -4
  73. package/src/validators/functional/_isFormatHostname.ts +4 -4
  74. package/src/validators/functional/_isFormatIdnEmail.ts +4 -4
  75. package/src/validators/functional/_isFormatIdnHostname.ts +4 -4
  76. package/src/validators/functional/_isFormatIpv4.ts +4 -4
  77. package/src/validators/functional/_isFormatIpv6.ts +4 -4
  78. package/src/validators/functional/_isFormatIri.ts +3 -3
  79. package/src/validators/functional/_isFormatIriReference.ts +4 -4
  80. package/src/validators/functional/_isFormatJsonPointer.ts +3 -3
  81. package/src/validators/functional/_isFormatPassword.ts +1 -1
  82. package/src/validators/functional/_isFormatRegex.ts +8 -8
  83. package/src/validators/functional/_isFormatRelativeJsonPointer.ts +4 -4
  84. package/src/validators/functional/_isFormatTime.ts +4 -4
  85. package/src/validators/functional/_isFormatUri.ts +6 -6
  86. package/src/validators/functional/_isFormatUriReference.ts +5 -5
  87. package/src/validators/functional/_isFormatUriTemplate.ts +4 -4
  88. package/src/validators/functional/_isFormatUrl.ts +4 -4
  89. package/src/validators/functional/_isFormatUuid.ts +3 -3
  90. package/src/validators/functional/_isUniqueItems.ts +159 -159
  91. package/src/validators/index.ts +14 -14
  92. package/src/validators/internal/IOpenApiValidatorContext.ts +17 -17
  93. package/src/validators/internal/OpenApiArrayValidator.ts +49 -49
  94. package/src/validators/internal/OpenApiBooleanValidator.ts +11 -11
  95. package/src/validators/internal/OpenApiConstantValidator.ts +11 -11
  96. package/src/validators/internal/OpenApiIntegerValidator.ts +49 -49
  97. package/src/validators/internal/OpenApiNumberValidator.ts +48 -48
  98. package/src/validators/internal/OpenApiObjectValidator.ts +83 -83
  99. package/src/validators/internal/OpenApiOneOfValidator.ts +309 -309
  100. package/src/validators/internal/OpenApiSchemaNamingRule.ts +124 -124
  101. package/src/validators/internal/OpenApiStationValidator.ts +115 -115
  102. package/src/validators/internal/OpenApiStringValidator.ts +88 -88
  103. package/src/validators/internal/OpenApiTupleValidator.ts +55 -55
@@ -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";
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
+ }