@nestia/sdk 1.3.1 → 1.3.3

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 (51) hide show
  1. package/assets/config/nestia.config.ts +70 -70
  2. package/lib/INestiaConfig.d.ts +13 -0
  3. package/lib/executable/internal/NestiaSdkConfig.js +6 -2
  4. package/lib/executable/internal/NestiaSdkConfig.js.map +1 -1
  5. package/lib/executable/sdk.js +11 -11
  6. package/lib/generates/SwaggerGenerator.js +9 -9
  7. package/lib/generates/internal/DistributionComposer.js +1 -1
  8. package/lib/generates/internal/DistributionComposer.js.map +1 -1
  9. package/lib/generates/internal/E2eFileProgrammer.js +12 -12
  10. package/lib/generates/internal/SdkFileProgrammer.js +3 -1
  11. package/lib/generates/internal/SdkFileProgrammer.js.map +1 -1
  12. package/lib/generates/internal/SdkFunctionProgrammer.js +24 -43
  13. package/lib/generates/internal/SdkFunctionProgrammer.js.map +1 -1
  14. package/package.json +4 -4
  15. package/src/INestiaConfig.ts +204 -190
  16. package/src/NestiaSdkApplication.ts +262 -262
  17. package/src/analyses/ControllerAnalyzer.ts +261 -261
  18. package/src/analyses/GenericAnalyzer.ts +53 -53
  19. package/src/analyses/ImportAnalyzer.ts +164 -164
  20. package/src/analyses/PathAnalyzer.ts +58 -58
  21. package/src/analyses/ReflectAnalyzer.ts +321 -321
  22. package/src/executable/internal/CommandParser.ts +15 -15
  23. package/src/executable/internal/NestiaConfigCompilerOptions.ts +18 -18
  24. package/src/executable/internal/NestiaSdkCommand.ts +156 -156
  25. package/src/executable/internal/NestiaSdkConfig.ts +36 -36
  26. package/src/executable/internal/nestia.config.getter.ts +12 -12
  27. package/src/executable/sdk.ts +70 -70
  28. package/src/generates/E2eGenerator.ts +67 -67
  29. package/src/generates/SdkGenerator.ts +56 -56
  30. package/src/generates/SwaggerGenerator.ts +504 -504
  31. package/src/generates/internal/DistributionComposer.ts +98 -97
  32. package/src/generates/internal/E2eFileProgrammer.ts +135 -135
  33. package/src/generates/internal/SdkFileProgrammer.ts +148 -144
  34. package/src/generates/internal/SdkFunctionProgrammer.ts +30 -52
  35. package/src/generates/internal/SdkRouteDirectory.ts +21 -21
  36. package/src/index.ts +4 -4
  37. package/src/module.ts +2 -2
  38. package/src/structures/IController.ts +31 -31
  39. package/src/structures/IRoute.ts +39 -39
  40. package/src/structures/ISwaggerDocument.ts +120 -120
  41. package/src/structures/ITypeTuple.ts +6 -6
  42. package/src/structures/MethodType.ts +11 -11
  43. package/src/structures/ParamCategory.ts +1 -1
  44. package/src/structures/TypeEntry.ts +22 -22
  45. package/src/utils/ArrayUtil.ts +26 -26
  46. package/src/utils/FileRetriever.ts +22 -22
  47. package/src/utils/ImportDictionary.ts +56 -56
  48. package/src/utils/MapUtil.ts +14 -14
  49. package/src/utils/NestiaConfigUtil.ts +21 -21
  50. package/src/utils/SourceFinder.ts +60 -60
  51. package/src/utils/StripEnums.ts +10 -10
@@ -1,504 +1,504 @@
1
- import fs from "fs";
2
- import NodePath from "path";
3
- import { Singleton } from "tstl/thread/Singleton";
4
- import { VariadicSingleton } from "tstl/thread/VariadicSingleton";
5
- import ts from "typescript";
6
-
7
- import typia from "typia";
8
- import { IJsonApplication, IJsonSchema } from "typia";
9
- import { MetadataCollection } from "typia/lib/factories/MetadataCollection";
10
- import { MetadataFactory } from "typia/lib/factories/MetadataFactory";
11
- import { Metadata } from "typia/lib/metadata/Metadata";
12
- import { ApplicationProgrammer } from "typia/lib/programmers/ApplicationProgrammer";
13
-
14
- import { INestiaConfig } from "../INestiaConfig";
15
- import { IRoute } from "../structures/IRoute";
16
- import { ISwaggerDocument } from "../structures/ISwaggerDocument";
17
- import { FileRetriever } from "../utils/FileRetriever";
18
- import { MapUtil } from "../utils/MapUtil";
19
-
20
- export namespace SwaggerGenerator {
21
- export const generate =
22
- (checker: ts.TypeChecker) =>
23
- (config: INestiaConfig.ISwaggerConfig) =>
24
- async (routeList: IRoute[]): Promise<void> => {
25
- console.log("Generating Swagger Documents");
26
-
27
- // PREPARE ASSETS
28
- const parsed: NodePath.ParsedPath = NodePath.parse(config.output);
29
- const directory: string = NodePath.dirname(parsed.dir);
30
- if (fs.existsSync(directory) === false)
31
- try {
32
- await fs.promises.mkdir(directory);
33
- } catch {}
34
- if (fs.existsSync(directory) === false)
35
- throw new Error(
36
- `Error on NestiaApplication.swagger(): failed to create output directory: ${directory}`,
37
- );
38
-
39
- const location: string = !!parsed.ext
40
- ? NodePath.resolve(config.output)
41
- : NodePath.join(
42
- NodePath.resolve(config.output),
43
- "swagger.json",
44
- );
45
-
46
- const collection: MetadataCollection = new MetadataCollection({
47
- replace: MetadataCollection.replace,
48
- });
49
-
50
- // CONSTRUCT SWAGGER DOCUMENTS
51
- const tupleList: Array<ISchemaTuple> = [];
52
- const swagger: ISwaggerDocument = await initialize(config);
53
- const pathDict: Map<string, ISwaggerDocument.IPath> = new Map();
54
-
55
- for (const route of routeList) {
56
- if (route.tags.find((tag) => tag.name === "internal")) continue;
57
-
58
- const path: ISwaggerDocument.IPath = MapUtil.take(
59
- pathDict,
60
- get_path(route.path, route.parameters),
61
- () => ({}),
62
- );
63
- path[route.method.toLowerCase()] = generate_route(
64
- checker,
65
- collection,
66
- tupleList,
67
- route,
68
- );
69
- }
70
- swagger.paths = {};
71
- for (const [path, routes] of pathDict) {
72
- swagger.paths[path] = routes;
73
- }
74
-
75
- // FILL JSON-SCHEMAS
76
- const application: IJsonApplication = ApplicationProgrammer.write({
77
- purpose: "swagger",
78
- })(tupleList.map(({ metadata }) => metadata));
79
- swagger.components = {
80
- ...(swagger.components ?? {}),
81
- ...(application.components ?? {}),
82
- };
83
- tupleList.forEach(({ schema }, index) => {
84
- Object.assign(schema, application.schemas[index]!);
85
- });
86
-
87
- // CONFIGURE SECURITY
88
- if (config.security) fill_security(config.security, swagger);
89
-
90
- // DO GENERATE
91
- await fs.promises.writeFile(
92
- location,
93
- JSON.stringify(swagger, null, 2),
94
- "utf8",
95
- );
96
- };
97
-
98
- /* ---------------------------------------------------------
99
- INITIALIZERS
100
- --------------------------------------------------------- */
101
- const initialize = async (
102
- config: INestiaConfig.ISwaggerConfig,
103
- ): Promise<ISwaggerDocument> => {
104
- const pack = new Singleton(
105
- async (): Promise<Partial<ISwaggerDocument.IInfo> | null> => {
106
- const location: string | null = await FileRetriever.file(
107
- "package.json",
108
- )(process.cwd());
109
- if (location === null) return null;
110
-
111
- try {
112
- const content: string = await fs.promises.readFile(
113
- location,
114
- "utf8",
115
- );
116
- const data = typia.assertParse<{
117
- name?: string;
118
- version?: string;
119
- description?: string;
120
- }>(content);
121
- return {
122
- title: data.name,
123
- version: data.version,
124
- description: data.description,
125
- };
126
- } catch {
127
- return null;
128
- }
129
- },
130
- );
131
-
132
- return {
133
- openapi: "3.0.1",
134
- servers: config.servers ?? [
135
- {
136
- url: "https://github.com/samchon/nestia",
137
- description: "insert your server url",
138
- },
139
- ],
140
- info: {
141
- version:
142
- config.info?.version ??
143
- (await pack.get())?.version ??
144
- "0.1.0",
145
- title:
146
- config.info?.title ??
147
- (await pack.get())?.title ??
148
- "Swagger Documents",
149
- description:
150
- config.info?.description ??
151
- (await pack.get())?.description ??
152
- "Generated by nestia - https://github.com/samchon/nestia",
153
- },
154
- paths: {},
155
- components: {},
156
- };
157
- };
158
-
159
- function get_path(path: string, parameters: IRoute.IParameter[]): string {
160
- const filtered: IRoute.IParameter[] = parameters.filter(
161
- (param) => param.category === "param" && !!param.field,
162
- );
163
- for (const param of filtered)
164
- path = path.replace(`:${param.field}`, `{${param.field}}`);
165
- return path;
166
- }
167
-
168
- function generate_route(
169
- checker: ts.TypeChecker,
170
- collection: MetadataCollection,
171
- tupleList: Array<ISchemaTuple>,
172
- route: IRoute,
173
- ): ISwaggerDocument.IRoute {
174
- const bodyParam = route.parameters.find(
175
- (param) => param.category === "body",
176
- );
177
-
178
- const getTagTexts = (name: string) =>
179
- route.tags
180
- .filter(
181
- (tag) =>
182
- tag.name === name &&
183
- tag.text &&
184
- tag.text.find(
185
- (elem) => elem.kind === "text" && elem.text.length,
186
- ) !== undefined,
187
- )
188
- .map(
189
- (tag) =>
190
- tag.text!.find((elem) => elem.kind === "text")!.text,
191
- );
192
-
193
- const description: string | undefined = route.description?.length
194
- ? route.description
195
- : undefined;
196
- const summary: string | undefined = (() => {
197
- if (description === undefined) return undefined;
198
-
199
- const [explicit] = getTagTexts("summary");
200
- if (explicit?.length) return explicit;
201
-
202
- const index: number = description.indexOf(".");
203
- if (index <= 0) return undefined;
204
-
205
- const content: string = description.substring(0, index).trim();
206
- return content.length ? content : undefined;
207
- })();
208
-
209
- return {
210
- tags: getTagTexts("tag"),
211
- parameters: route.parameters
212
- .filter((param) => param.category !== "body")
213
- .map((param) =>
214
- generate_parameter(
215
- checker,
216
- collection,
217
- tupleList,
218
- route,
219
- param,
220
- ),
221
- ),
222
- requestBody: bodyParam
223
- ? generate_request_body(
224
- checker,
225
- collection,
226
- tupleList,
227
- route,
228
- bodyParam,
229
- )
230
- : undefined,
231
- responses: generate_response_body(
232
- checker,
233
- collection,
234
- tupleList,
235
- route,
236
- ),
237
- summary,
238
- description,
239
- "x-nestia-namespace": [
240
- ...route.path
241
- .split("/")
242
- .filter((str) => str.length && str[0] !== ":"),
243
- route.name,
244
- ].join("."),
245
- "x-nestia-jsDocTags": route.tags,
246
- };
247
- }
248
-
249
- function fill_security(
250
- security: Required<INestiaConfig.ISwaggerConfig>["security"],
251
- swagger: ISwaggerDocument,
252
- ): void {
253
- swagger.security = [{}];
254
- swagger.components.securitySchemes = {};
255
-
256
- for (const [key, value] of Object.entries(security)) {
257
- swagger.security[0]![key] = [];
258
- swagger.components.securitySchemes[key] = emend_security(value);
259
- }
260
- }
261
-
262
- function emend_security(
263
- input: INestiaConfig.ISwaggerConfig.ISecurityScheme,
264
- ): ISwaggerDocument.ISecurityScheme {
265
- if (input.type === "apiKey")
266
- return {
267
- ...input,
268
- in: input.in ?? "header",
269
- name: input.name ?? "Authorization",
270
- };
271
- return input;
272
- }
273
-
274
- /* ---------------------------------------------------------
275
- REQUEST & RESPONSE
276
- --------------------------------------------------------- */
277
- function generate_parameter(
278
- checker: ts.TypeChecker,
279
- collection: MetadataCollection,
280
- tupleList: Array<ISchemaTuple>,
281
- route: IRoute,
282
- parameter: IRoute.IParameter,
283
- ): ISwaggerDocument.IParameter {
284
- const schema: IJsonSchema | null = generate_schema(
285
- checker,
286
- collection,
287
- tupleList,
288
- parameter.type.type,
289
- );
290
- if (schema === null)
291
- throw new Error(
292
- `Error on NestiaApplication.swagger(): invalid parameter type on ${route.symbol}#${parameter.name}`,
293
- );
294
-
295
- return {
296
- name: parameter.field ?? parameter.name,
297
- in: parameter.category === "param" ? "path" : parameter.category,
298
- description:
299
- get_parametric_description(route, "param", parameter.name) ||
300
- "",
301
- schema,
302
- required: required(parameter.type.type),
303
- };
304
- }
305
-
306
- function generate_request_body(
307
- checker: ts.TypeChecker,
308
- collection: MetadataCollection,
309
- tupleList: Array<ISchemaTuple>,
310
- route: IRoute,
311
- parameter: IRoute.IParameter,
312
- ): ISwaggerDocument.IRequestBody {
313
- const schema: IJsonSchema | null = generate_schema(
314
- checker,
315
- collection,
316
- tupleList,
317
- parameter.type.type,
318
- );
319
- if (schema === null)
320
- throw new Error(
321
- `Error on NestiaApplication.sdk(): invalid request body type on ${route.symbol}.`,
322
- );
323
-
324
- return {
325
- description:
326
- warning.get(parameter.encrypted).get("request") +
327
- (get_parametric_description(route, "param", parameter.name) ??
328
- ""),
329
- content: {
330
- "application/json": {
331
- schema,
332
- },
333
- },
334
- required: true,
335
- "x-nestia-encrypted": parameter.encrypted,
336
- };
337
- }
338
-
339
- function generate_response_body(
340
- checker: ts.TypeChecker,
341
- collection: MetadataCollection,
342
- tupleList: Array<ISchemaTuple>,
343
- route: IRoute,
344
- ): ISwaggerDocument.IResponseBody {
345
- // OUTPUT WITH SUCCESS STATUS
346
- const status: string =
347
- route.status !== undefined
348
- ? String(route.status)
349
- : route.method === "GET" || route.method === "DELETE"
350
- ? "200"
351
- : "201";
352
- const schema: IJsonSchema | null = generate_schema(
353
- checker,
354
- collection,
355
- tupleList,
356
- route.output.type,
357
- );
358
- const success: ISwaggerDocument.IResponseBody = {
359
- [status]: {
360
- description:
361
- warning.get(route.encrypted).get("response", route.method) +
362
- (get_parametric_description(route, "return") ??
363
- get_parametric_description(route, "returns") ??
364
- ""),
365
- content:
366
- schema === null || route.output.name === "void"
367
- ? undefined
368
- : {
369
- "application/json": {
370
- schema,
371
- },
372
- },
373
- "x-nestia-encrypted": route.encrypted,
374
- },
375
- };
376
-
377
- // EXCEPTION STATUSES
378
- const exceptions: ISwaggerDocument.IResponseBody = Object.fromEntries(
379
- route.tags
380
- .filter(
381
- (tag) =>
382
- (tag.name === "throw" || tag.name === "throws") &&
383
- tag.text &&
384
- tag.text.find(
385
- (elem) =>
386
- elem.kind === "text" &&
387
- isNaN(
388
- Number(
389
- elem.text
390
- .split(" ")
391
- .map((str) => str.trim())[0],
392
- ),
393
- ) === false,
394
- ) !== undefined,
395
- )
396
- .map((tag) => {
397
- const text: string = tag.text!.find(
398
- (elem) => elem.kind === "text",
399
- )!.text;
400
- const elements: string[] = text
401
- .split(" ")
402
- .map((str) => str.trim());
403
-
404
- return [
405
- elements[0],
406
- {
407
- description: elements.slice(1).join(" "),
408
- },
409
- ];
410
- }),
411
- );
412
- return { ...exceptions, ...success };
413
- }
414
-
415
- /* ---------------------------------------------------------
416
- UTILS
417
- --------------------------------------------------------- */
418
- function generate_schema(
419
- checker: ts.TypeChecker,
420
- collection: MetadataCollection,
421
- tupleList: Array<ISchemaTuple>,
422
- type: ts.Type,
423
- ): IJsonSchema | null {
424
- const metadata: Metadata = MetadataFactory.analyze(checker)({
425
- resolve: false,
426
- constant: true,
427
- absorb: false,
428
- })(collection)(type);
429
- if (metadata.empty() && metadata.nullable === false) return null;
430
-
431
- const schema: IJsonSchema = {} as IJsonSchema;
432
- tupleList.push({ metadata, schema });
433
- return schema;
434
- }
435
-
436
- function get_parametric_description(
437
- route: IRoute,
438
- tagName: string,
439
- parameterName?: string,
440
- ): string | undefined {
441
- const parametric: (elem: ts.JSDocTagInfo) => boolean = parameterName
442
- ? (tag) =>
443
- tag.text!.find(
444
- (elem) =>
445
- elem.kind === "parameterName" &&
446
- elem.text === parameterName,
447
- ) !== undefined
448
- : () => true;
449
-
450
- const tag: ts.JSDocTagInfo | undefined = route.tags.find(
451
- (tag) => tag.name === tagName && tag.text && parametric(tag),
452
- );
453
- return tag && tag.text
454
- ? tag.text.find((elem) => elem.kind === "text")?.text
455
- : undefined;
456
- }
457
- }
458
-
459
- const required = (type: ts.Type): boolean => {
460
- if (type.isUnion()) return type.types.every((type) => required(type));
461
- const obstacle = (other: ts.TypeFlags) => (type.getFlags() & other) === 0;
462
- return (
463
- obstacle(ts.TypeFlags.Undefined) &&
464
- obstacle(ts.TypeFlags.Never) &&
465
- obstacle(ts.TypeFlags.Void) &&
466
- obstacle(ts.TypeFlags.VoidLike)
467
- );
468
- };
469
-
470
- const warning = new VariadicSingleton((encrypted: boolean) => {
471
- if (encrypted === false) return new Singleton(() => "");
472
-
473
- return new VariadicSingleton(
474
- (type: "request" | "response", method?: string) => {
475
- const summary =
476
- type === "request"
477
- ? "Request body must be encrypted."
478
- : "Response data have been encrypted.";
479
-
480
- const component =
481
- type === "request"
482
- ? "[EncryptedBody](https://github.com/samchon/@nestia/core#encryptedbody)"
483
- : `[EncryptedRoute.${method![0].toUpperCase()}.${method!
484
- .substring(1)
485
- .toLowerCase()}](https://github.com/samchon/@nestia/core#encryptedroute)`;
486
-
487
- return `## Warning
488
- ${summary}
489
-
490
- The ${type} body data would be encrypted as "AES-128(256) / CBC mode / PKCS#5 Padding / Base64 Encoding", through the ${component} component.
491
-
492
- Therefore, just utilize this swagger editor only for referencing. If you need to call the real API, using [SDK](https://github.com/samchon/nestia#software-development-kit) would be much better.
493
-
494
- -----------------
495
-
496
- `;
497
- },
498
- );
499
- });
500
-
501
- interface ISchemaTuple {
502
- metadata: Metadata;
503
- schema: IJsonSchema;
504
- }
1
+ import fs from "fs";
2
+ import NodePath from "path";
3
+ import { Singleton } from "tstl/thread/Singleton";
4
+ import { VariadicSingleton } from "tstl/thread/VariadicSingleton";
5
+ import ts from "typescript";
6
+
7
+ import typia from "typia";
8
+ import { IJsonApplication, IJsonSchema } from "typia";
9
+ import { MetadataCollection } from "typia/lib/factories/MetadataCollection";
10
+ import { MetadataFactory } from "typia/lib/factories/MetadataFactory";
11
+ import { Metadata } from "typia/lib/metadata/Metadata";
12
+ import { ApplicationProgrammer } from "typia/lib/programmers/ApplicationProgrammer";
13
+
14
+ import { INestiaConfig } from "../INestiaConfig";
15
+ import { IRoute } from "../structures/IRoute";
16
+ import { ISwaggerDocument } from "../structures/ISwaggerDocument";
17
+ import { FileRetriever } from "../utils/FileRetriever";
18
+ import { MapUtil } from "../utils/MapUtil";
19
+
20
+ export namespace SwaggerGenerator {
21
+ export const generate =
22
+ (checker: ts.TypeChecker) =>
23
+ (config: INestiaConfig.ISwaggerConfig) =>
24
+ async (routeList: IRoute[]): Promise<void> => {
25
+ console.log("Generating Swagger Documents");
26
+
27
+ // PREPARE ASSETS
28
+ const parsed: NodePath.ParsedPath = NodePath.parse(config.output);
29
+ const directory: string = NodePath.dirname(parsed.dir);
30
+ if (fs.existsSync(directory) === false)
31
+ try {
32
+ await fs.promises.mkdir(directory);
33
+ } catch {}
34
+ if (fs.existsSync(directory) === false)
35
+ throw new Error(
36
+ `Error on NestiaApplication.swagger(): failed to create output directory: ${directory}`,
37
+ );
38
+
39
+ const location: string = !!parsed.ext
40
+ ? NodePath.resolve(config.output)
41
+ : NodePath.join(
42
+ NodePath.resolve(config.output),
43
+ "swagger.json",
44
+ );
45
+
46
+ const collection: MetadataCollection = new MetadataCollection({
47
+ replace: MetadataCollection.replace,
48
+ });
49
+
50
+ // CONSTRUCT SWAGGER DOCUMENTS
51
+ const tupleList: Array<ISchemaTuple> = [];
52
+ const swagger: ISwaggerDocument = await initialize(config);
53
+ const pathDict: Map<string, ISwaggerDocument.IPath> = new Map();
54
+
55
+ for (const route of routeList) {
56
+ if (route.tags.find((tag) => tag.name === "internal")) continue;
57
+
58
+ const path: ISwaggerDocument.IPath = MapUtil.take(
59
+ pathDict,
60
+ get_path(route.path, route.parameters),
61
+ () => ({}),
62
+ );
63
+ path[route.method.toLowerCase()] = generate_route(
64
+ checker,
65
+ collection,
66
+ tupleList,
67
+ route,
68
+ );
69
+ }
70
+ swagger.paths = {};
71
+ for (const [path, routes] of pathDict) {
72
+ swagger.paths[path] = routes;
73
+ }
74
+
75
+ // FILL JSON-SCHEMAS
76
+ const application: IJsonApplication = ApplicationProgrammer.write({
77
+ purpose: "swagger",
78
+ })(tupleList.map(({ metadata }) => metadata));
79
+ swagger.components = {
80
+ ...(swagger.components ?? {}),
81
+ ...(application.components ?? {}),
82
+ };
83
+ tupleList.forEach(({ schema }, index) => {
84
+ Object.assign(schema, application.schemas[index]!);
85
+ });
86
+
87
+ // CONFIGURE SECURITY
88
+ if (config.security) fill_security(config.security, swagger);
89
+
90
+ // DO GENERATE
91
+ await fs.promises.writeFile(
92
+ location,
93
+ JSON.stringify(swagger, null, 2),
94
+ "utf8",
95
+ );
96
+ };
97
+
98
+ /* ---------------------------------------------------------
99
+ INITIALIZERS
100
+ --------------------------------------------------------- */
101
+ const initialize = async (
102
+ config: INestiaConfig.ISwaggerConfig,
103
+ ): Promise<ISwaggerDocument> => {
104
+ const pack = new Singleton(
105
+ async (): Promise<Partial<ISwaggerDocument.IInfo> | null> => {
106
+ const location: string | null = await FileRetriever.file(
107
+ "package.json",
108
+ )(process.cwd());
109
+ if (location === null) return null;
110
+
111
+ try {
112
+ const content: string = await fs.promises.readFile(
113
+ location,
114
+ "utf8",
115
+ );
116
+ const data = typia.assertParse<{
117
+ name?: string;
118
+ version?: string;
119
+ description?: string;
120
+ }>(content);
121
+ return {
122
+ title: data.name,
123
+ version: data.version,
124
+ description: data.description,
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ },
130
+ );
131
+
132
+ return {
133
+ openapi: "3.0.1",
134
+ servers: config.servers ?? [
135
+ {
136
+ url: "https://github.com/samchon/nestia",
137
+ description: "insert your server url",
138
+ },
139
+ ],
140
+ info: {
141
+ version:
142
+ config.info?.version ??
143
+ (await pack.get())?.version ??
144
+ "0.1.0",
145
+ title:
146
+ config.info?.title ??
147
+ (await pack.get())?.title ??
148
+ "Swagger Documents",
149
+ description:
150
+ config.info?.description ??
151
+ (await pack.get())?.description ??
152
+ "Generated by nestia - https://github.com/samchon/nestia",
153
+ },
154
+ paths: {},
155
+ components: {},
156
+ };
157
+ };
158
+
159
+ function get_path(path: string, parameters: IRoute.IParameter[]): string {
160
+ const filtered: IRoute.IParameter[] = parameters.filter(
161
+ (param) => param.category === "param" && !!param.field,
162
+ );
163
+ for (const param of filtered)
164
+ path = path.replace(`:${param.field}`, `{${param.field}}`);
165
+ return path;
166
+ }
167
+
168
+ function generate_route(
169
+ checker: ts.TypeChecker,
170
+ collection: MetadataCollection,
171
+ tupleList: Array<ISchemaTuple>,
172
+ route: IRoute,
173
+ ): ISwaggerDocument.IRoute {
174
+ const bodyParam = route.parameters.find(
175
+ (param) => param.category === "body",
176
+ );
177
+
178
+ const getTagTexts = (name: string) =>
179
+ route.tags
180
+ .filter(
181
+ (tag) =>
182
+ tag.name === name &&
183
+ tag.text &&
184
+ tag.text.find(
185
+ (elem) => elem.kind === "text" && elem.text.length,
186
+ ) !== undefined,
187
+ )
188
+ .map(
189
+ (tag) =>
190
+ tag.text!.find((elem) => elem.kind === "text")!.text,
191
+ );
192
+
193
+ const description: string | undefined = route.description?.length
194
+ ? route.description
195
+ : undefined;
196
+ const summary: string | undefined = (() => {
197
+ if (description === undefined) return undefined;
198
+
199
+ const [explicit] = getTagTexts("summary");
200
+ if (explicit?.length) return explicit;
201
+
202
+ const index: number = description.indexOf(".");
203
+ if (index <= 0) return undefined;
204
+
205
+ const content: string = description.substring(0, index).trim();
206
+ return content.length ? content : undefined;
207
+ })();
208
+
209
+ return {
210
+ tags: getTagTexts("tag"),
211
+ parameters: route.parameters
212
+ .filter((param) => param.category !== "body")
213
+ .map((param) =>
214
+ generate_parameter(
215
+ checker,
216
+ collection,
217
+ tupleList,
218
+ route,
219
+ param,
220
+ ),
221
+ ),
222
+ requestBody: bodyParam
223
+ ? generate_request_body(
224
+ checker,
225
+ collection,
226
+ tupleList,
227
+ route,
228
+ bodyParam,
229
+ )
230
+ : undefined,
231
+ responses: generate_response_body(
232
+ checker,
233
+ collection,
234
+ tupleList,
235
+ route,
236
+ ),
237
+ summary,
238
+ description,
239
+ "x-nestia-namespace": [
240
+ ...route.path
241
+ .split("/")
242
+ .filter((str) => str.length && str[0] !== ":"),
243
+ route.name,
244
+ ].join("."),
245
+ "x-nestia-jsDocTags": route.tags,
246
+ };
247
+ }
248
+
249
+ function fill_security(
250
+ security: Required<INestiaConfig.ISwaggerConfig>["security"],
251
+ swagger: ISwaggerDocument,
252
+ ): void {
253
+ swagger.security = [{}];
254
+ swagger.components.securitySchemes = {};
255
+
256
+ for (const [key, value] of Object.entries(security)) {
257
+ swagger.security[0]![key] = [];
258
+ swagger.components.securitySchemes[key] = emend_security(value);
259
+ }
260
+ }
261
+
262
+ function emend_security(
263
+ input: INestiaConfig.ISwaggerConfig.ISecurityScheme,
264
+ ): ISwaggerDocument.ISecurityScheme {
265
+ if (input.type === "apiKey")
266
+ return {
267
+ ...input,
268
+ in: input.in ?? "header",
269
+ name: input.name ?? "Authorization",
270
+ };
271
+ return input;
272
+ }
273
+
274
+ /* ---------------------------------------------------------
275
+ REQUEST & RESPONSE
276
+ --------------------------------------------------------- */
277
+ function generate_parameter(
278
+ checker: ts.TypeChecker,
279
+ collection: MetadataCollection,
280
+ tupleList: Array<ISchemaTuple>,
281
+ route: IRoute,
282
+ parameter: IRoute.IParameter,
283
+ ): ISwaggerDocument.IParameter {
284
+ const schema: IJsonSchema | null = generate_schema(
285
+ checker,
286
+ collection,
287
+ tupleList,
288
+ parameter.type.type,
289
+ );
290
+ if (schema === null)
291
+ throw new Error(
292
+ `Error on NestiaApplication.swagger(): invalid parameter type on ${route.symbol}#${parameter.name}`,
293
+ );
294
+
295
+ return {
296
+ name: parameter.field ?? parameter.name,
297
+ in: parameter.category === "param" ? "path" : parameter.category,
298
+ description:
299
+ get_parametric_description(route, "param", parameter.name) ||
300
+ "",
301
+ schema,
302
+ required: required(parameter.type.type),
303
+ };
304
+ }
305
+
306
+ function generate_request_body(
307
+ checker: ts.TypeChecker,
308
+ collection: MetadataCollection,
309
+ tupleList: Array<ISchemaTuple>,
310
+ route: IRoute,
311
+ parameter: IRoute.IParameter,
312
+ ): ISwaggerDocument.IRequestBody {
313
+ const schema: IJsonSchema | null = generate_schema(
314
+ checker,
315
+ collection,
316
+ tupleList,
317
+ parameter.type.type,
318
+ );
319
+ if (schema === null)
320
+ throw new Error(
321
+ `Error on NestiaApplication.sdk(): invalid request body type on ${route.symbol}.`,
322
+ );
323
+
324
+ return {
325
+ description:
326
+ warning.get(parameter.encrypted).get("request") +
327
+ (get_parametric_description(route, "param", parameter.name) ??
328
+ ""),
329
+ content: {
330
+ "application/json": {
331
+ schema,
332
+ },
333
+ },
334
+ required: true,
335
+ "x-nestia-encrypted": parameter.encrypted,
336
+ };
337
+ }
338
+
339
+ function generate_response_body(
340
+ checker: ts.TypeChecker,
341
+ collection: MetadataCollection,
342
+ tupleList: Array<ISchemaTuple>,
343
+ route: IRoute,
344
+ ): ISwaggerDocument.IResponseBody {
345
+ // OUTPUT WITH SUCCESS STATUS
346
+ const status: string =
347
+ route.status !== undefined
348
+ ? String(route.status)
349
+ : route.method === "GET" || route.method === "DELETE"
350
+ ? "200"
351
+ : "201";
352
+ const schema: IJsonSchema | null = generate_schema(
353
+ checker,
354
+ collection,
355
+ tupleList,
356
+ route.output.type,
357
+ );
358
+ const success: ISwaggerDocument.IResponseBody = {
359
+ [status]: {
360
+ description:
361
+ warning.get(route.encrypted).get("response", route.method) +
362
+ (get_parametric_description(route, "return") ??
363
+ get_parametric_description(route, "returns") ??
364
+ ""),
365
+ content:
366
+ schema === null || route.output.name === "void"
367
+ ? undefined
368
+ : {
369
+ "application/json": {
370
+ schema,
371
+ },
372
+ },
373
+ "x-nestia-encrypted": route.encrypted,
374
+ },
375
+ };
376
+
377
+ // EXCEPTION STATUSES
378
+ const exceptions: ISwaggerDocument.IResponseBody = Object.fromEntries(
379
+ route.tags
380
+ .filter(
381
+ (tag) =>
382
+ (tag.name === "throw" || tag.name === "throws") &&
383
+ tag.text &&
384
+ tag.text.find(
385
+ (elem) =>
386
+ elem.kind === "text" &&
387
+ isNaN(
388
+ Number(
389
+ elem.text
390
+ .split(" ")
391
+ .map((str) => str.trim())[0],
392
+ ),
393
+ ) === false,
394
+ ) !== undefined,
395
+ )
396
+ .map((tag) => {
397
+ const text: string = tag.text!.find(
398
+ (elem) => elem.kind === "text",
399
+ )!.text;
400
+ const elements: string[] = text
401
+ .split(" ")
402
+ .map((str) => str.trim());
403
+
404
+ return [
405
+ elements[0],
406
+ {
407
+ description: elements.slice(1).join(" "),
408
+ },
409
+ ];
410
+ }),
411
+ );
412
+ return { ...exceptions, ...success };
413
+ }
414
+
415
+ /* ---------------------------------------------------------
416
+ UTILS
417
+ --------------------------------------------------------- */
418
+ function generate_schema(
419
+ checker: ts.TypeChecker,
420
+ collection: MetadataCollection,
421
+ tupleList: Array<ISchemaTuple>,
422
+ type: ts.Type,
423
+ ): IJsonSchema | null {
424
+ const metadata: Metadata = MetadataFactory.analyze(checker)({
425
+ resolve: false,
426
+ constant: true,
427
+ absorb: false,
428
+ })(collection)(type);
429
+ if (metadata.empty() && metadata.nullable === false) return null;
430
+
431
+ const schema: IJsonSchema = {} as IJsonSchema;
432
+ tupleList.push({ metadata, schema });
433
+ return schema;
434
+ }
435
+
436
+ function get_parametric_description(
437
+ route: IRoute,
438
+ tagName: string,
439
+ parameterName?: string,
440
+ ): string | undefined {
441
+ const parametric: (elem: ts.JSDocTagInfo) => boolean = parameterName
442
+ ? (tag) =>
443
+ tag.text!.find(
444
+ (elem) =>
445
+ elem.kind === "parameterName" &&
446
+ elem.text === parameterName,
447
+ ) !== undefined
448
+ : () => true;
449
+
450
+ const tag: ts.JSDocTagInfo | undefined = route.tags.find(
451
+ (tag) => tag.name === tagName && tag.text && parametric(tag),
452
+ );
453
+ return tag && tag.text
454
+ ? tag.text.find((elem) => elem.kind === "text")?.text
455
+ : undefined;
456
+ }
457
+ }
458
+
459
+ const required = (type: ts.Type): boolean => {
460
+ if (type.isUnion()) return type.types.every((type) => required(type));
461
+ const obstacle = (other: ts.TypeFlags) => (type.getFlags() & other) === 0;
462
+ return (
463
+ obstacle(ts.TypeFlags.Undefined) &&
464
+ obstacle(ts.TypeFlags.Never) &&
465
+ obstacle(ts.TypeFlags.Void) &&
466
+ obstacle(ts.TypeFlags.VoidLike)
467
+ );
468
+ };
469
+
470
+ const warning = new VariadicSingleton((encrypted: boolean) => {
471
+ if (encrypted === false) return new Singleton(() => "");
472
+
473
+ return new VariadicSingleton(
474
+ (type: "request" | "response", method?: string) => {
475
+ const summary =
476
+ type === "request"
477
+ ? "Request body must be encrypted."
478
+ : "Response data have been encrypted.";
479
+
480
+ const component =
481
+ type === "request"
482
+ ? "[EncryptedBody](https://github.com/samchon/@nestia/core#encryptedbody)"
483
+ : `[EncryptedRoute.${method![0].toUpperCase()}.${method!
484
+ .substring(1)
485
+ .toLowerCase()}](https://github.com/samchon/@nestia/core#encryptedroute)`;
486
+
487
+ return `## Warning
488
+ ${summary}
489
+
490
+ The ${type} body data would be encrypted as "AES-128(256) / CBC mode / PKCS#5 Padding / Base64 Encoding", through the ${component} component.
491
+
492
+ Therefore, just utilize this swagger editor only for referencing. If you need to call the real API, using [SDK](https://github.com/samchon/nestia#software-development-kit) would be much better.
493
+
494
+ -----------------
495
+
496
+ `;
497
+ },
498
+ );
499
+ });
500
+
501
+ interface ISchemaTuple {
502
+ metadata: Metadata;
503
+ schema: IJsonSchema;
504
+ }