Skip to content

Commit

Permalink
Runtime swagger document composer.
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Aug 27, 2024
1 parent 9995cde commit 8ac3f83
Show file tree
Hide file tree
Showing 21 changed files with 210 additions and 27 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "@nestia/station",
"version": "3.11.3",
"version": "3.12.0-dev.20240827",
"description": "Nestia station",
"scripts": {
"build": "node build/index.js",
Expand Down
9 changes: 9 additions & 0 deletions packages/cli/src/internal/ArgumentParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export namespace ArgumentParser {
export interface IArguments {
manager: "npm" | "pnpm" | "yarn";
project: string | null;
swagger: boolean;
}

export async function parse(pack: PackageManager): Promise<IArguments> {
Expand All @@ -17,6 +18,10 @@ export namespace ArgumentParser {
"--project [project]",
"tsconfig.json file location",
);
commander.program.option(
"--swagger [boolean]",
"transform runtime swagger",
);

// INTERNAL PROCEDURES
const questioned = { value: false };
Expand Down Expand Up @@ -90,6 +95,10 @@ export namespace ArgumentParser {
);
pack.manager = options.manager;
options.project ??= await configure();
options.swagger = options.swagger
? (options.swagger as any) === "true"
: (await select("swagger")("Transform Runtime Swagger")(["Y", "N"])) ===
"Y";

if (questioned.value) console.log("");
return options as IArguments;
Expand Down
15 changes: 15 additions & 0 deletions packages/cli/src/internal/PluginConfigurator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,15 @@ export namespace PluginConfigurator {
p !== null &&
p.transform === "typia/lib/transform",
);
const swagger: boolean =
args.swagger === false
? true
: !!plugins.find(
(p) =>
typeof p === "object" &&
p !== null &&
p.transform === "@nestia/sdk/lib/transform",
);
if (
strictNullChecks !== false &&
(strict === true || strictNullChecks === true) &&
Expand Down Expand Up @@ -92,6 +101,12 @@ export namespace PluginConfigurator {
"stringify": "assert"
}`) as comments.CommentObject,
);
if (swagger === false)
plugins.push(
comments.parse(
`{ "transform": "@nestia/sdk/lib/transform" }`,
) as comments.CommentObject,
);
if (typia === undefined)
plugins.push(
comments.parse(
Expand Down
6 changes: 3 additions & 3 deletions packages/core/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/core",
"version": "3.11.3",
"version": "3.12.0-dev.20240827",
"description": "Super-fast validation decorators of NestJS",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -36,7 +36,7 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/fetcher": "^3.11.3",
"@nestia/fetcher": "^3.12.0-dev.20240827",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"@samchon/openapi": "^0.4.6",
Expand All @@ -53,7 +53,7 @@
"ws": "^7.5.3"
},
"peerDependencies": {
"@nestia/fetcher": ">=3.11.3",
"@nestia/fetcher": ">=3.12.0-dev.20240827",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
2 changes: 1 addition & 1 deletion packages/fetcher/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/fetcher",
"version": "3.11.3",
"version": "3.12.0-dev.20240827",
"description": "Fetcher library of Nestia SDK",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down
10 changes: 5 additions & 5 deletions packages/sdk/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@nestia/sdk",
"version": "3.11.3",
"version": "3.12.0-dev.20240827",
"description": "Nestia SDK and Swagger generator",
"main": "lib/index.js",
"typings": "lib/index.d.ts",
Expand Down Expand Up @@ -32,8 +32,8 @@
},
"homepage": "https://nestia.io",
"dependencies": {
"@nestia/core": "^3.11.3",
"@nestia/fetcher": "^3.11.3",
"@nestia/core": "^3.12.0-dev.20240827",
"@nestia/fetcher": "^3.12.0-dev.20240827",
"@samchon/openapi": "^0.4.6",
"@wrtnio/openai-function-schema": "^0.2.3",
"cli": "^1.0.1",
Expand All @@ -48,8 +48,8 @@
"typia": "^6.9.0"
},
"peerDependencies": {
"@nestia/core": ">=3.11.3",
"@nestia/fetcher": ">=3.11.3",
"@nestia/core": ">=3.12.0-dev.20240827",
"@nestia/fetcher": ">=3.12.0-dev.20240827",
"@nestjs/common": ">=7.0.1",
"@nestjs/core": ">=7.0.1",
"reflect-metadata": ">=0.1.12",
Expand Down
138 changes: 138 additions & 0 deletions packages/sdk/src/NestiaSwaggerComposer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { INestApplication } from "@nestjs/common";
import { OpenApi, OpenApiV3, SwaggerV2 } from "@samchon/openapi";
import path from "path";
import { TreeMap } from "tstl";
import { IMetadataDictionary } from "typia/lib/schemas/metadata/IMetadataDictionary";

import { INestiaConfig } from "./INestiaConfig";
import { AccessorAnalyzer } from "./analyses/AccessorAnalyzer";
import { ConfigAnalyzer } from "./analyses/ConfigAnalyzer";
import { PathAnalyzer } from "./analyses/PathAnalyzer";
import { ReflectControllerAnalyzer } from "./analyses/ReflectControllerAnalyzer";
import { TypedHttpRouteAnalyzer } from "./analyses/TypedHttpRouteAnalyzer";
import { SwaggerGenerator } from "./generates/SwaggerGenerator";
import { INestiaProject } from "./structures/INestiaProject";
import { INestiaSdkInput } from "./structures/INestiaSdkInput";
import { IReflectController } from "./structures/IReflectController";
import { IReflectOperationError } from "./structures/IReflectOperationError";
import { ITypedHttpRoute } from "./structures/ITypedHttpRoute";
import { IOperationMetadata } from "./transformers/IOperationMetadata";
import { VersioningStrategy } from "./utils/VersioningStrategy";

export namespace NestiaSwaggerComposer {
export const document = async (
app: INestApplication,
config: Omit<INestiaConfig.ISwaggerConfig, "output">,
): Promise<OpenApi.IDocument | OpenApiV3.IDocument | SwaggerV2.IDocument> => {
const input: INestiaSdkInput = await ConfigAnalyzer.application(app);
const document: OpenApi.IDocument = await SwaggerGenerator.compose({
config,
routes: analyze(input),
document: await SwaggerGenerator.initialize(config),
});
return config.openapi === "2.0"
? OpenApi.downgrade(document, "2.0")
: config.openapi === "3.0"
? OpenApi.downgrade(document, "3.0")
: document;
};

const analyze = (input: INestiaSdkInput): ITypedHttpRoute[] => {
// GET REFLECT CONTROLLERS
const unique: WeakSet<any> = new WeakSet();
const project: Omit<INestiaProject, "config"> = {
input,
checker: null!,
errors: [],
warnings: [],
};
const controllers: IReflectController[] = project.input.controllers
.map((c) =>
ReflectControllerAnalyzer.analyze({ project, controller: c, unique }),
)
.filter((c): c is IReflectController => c !== null);
if (project.errors.length)
throw report({ type: "error", errors: project.errors });

// METADATA COMPONENTS
const collection: IMetadataDictionary =
TypedHttpRouteAnalyzer.dictionary(controllers);

// CONVERT TO TYPED OPERATIONS
const globalPrefix: string = project.input.globalPrefix?.prefix ?? "";
const routes: ITypedHttpRoute[] = [];
for (const c of controllers)
for (const o of c.operations) {
const pathList: Set<string> = new Set();
const versions: string[] = VersioningStrategy.merge(project)([
...(c.versions ?? []),
...(o.versions ?? []),
]);
for (const v of versions)
for (const prefix of wrapPaths(c.prefixes))
for (const cPath of wrapPaths(c.paths))
for (const filePath of wrapPaths(o.paths))
pathList.add(
PathAnalyzer.join(globalPrefix, v, prefix, cPath, filePath),
);
if (o.protocol === "http")
routes.push(
...TypedHttpRouteAnalyzer.analyze({
controller: c,
errors: project.errors,
dictionary: collection,
operation: o,
paths: Array.from(pathList),
}),
);
}
AccessorAnalyzer.analyze(routes);
return routes;
};
}

const report = (props: {
type: "error" | "warning";
errors: IReflectOperationError[];
}): void => {
const map: TreeMap<
IReflectOperationError.Key,
Array<string | IOperationMetadata.IError>
> = new TreeMap();
for (const e of props.errors)
map.take(new IReflectOperationError.Key(e), () => []).push(...e.contents);

const messages: string[] = [];
for (const {
first: { error },
second: contents,
} of map) {
if (error.contents.length === 0) continue;
const location: string = path.relative(process.cwd(), error.file);
messages.push(
[
`${location} - `,
error.class,
...(error.function !== null ? [`.${error.function}()`] : [""]),
...(error.from !== null ? [` from ${error.from}`] : [""]),
":\n",
contents
.map((c) => {
if (typeof c === "string") return ` - ${c}`;
else
return [
c.accessor
? ` - ${c.name}: `
: ` - ${c.name} (${c.accessor}): `,
...c.messages.map((msg) => ` - ${msg}`),
].join("\n");
})
.join("\n"),
].join(""),
);
}
throw new Error(`Error on NestiaSwaggerComposer.compose():\n${messages}`);
};

const wrapPaths = (paths: string[]): string[] =>
paths.length === 0 ? [""] : paths;
6 changes: 3 additions & 3 deletions packages/sdk/src/analyses/ConfigAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export namespace ConfigAnalyzer {
): Promise<INestiaSdkInput> => {
return MapUtil.take(memory, config, async () => {
if (typeof config.input === "function")
return analyze_application(await config.input());
return application(await config.input());

const sources: string[] = await SourceFinder.find({
include: Array.isArray(config.input)
Expand All @@ -31,7 +31,7 @@ export namespace ConfigAnalyzer {
: [config.input],
exclude:
typeof config.input === "object" && !Array.isArray(config.input)
? config.input.exclude ?? []
? (config.input.exclude ?? [])
: [],
filter: filter(config),
});
Expand All @@ -54,7 +54,7 @@ export namespace ConfigAnalyzer {
});
};

const analyze_application = async (
export const application = async (
app: INestApplication,
): Promise<INestiaSdkInput> => {
const container: NestContainer = (app as any).container as NestContainer;
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/analyses/ReflectControllerAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import { ReflectWebSocketOperationAnalyzer } from "./ReflectWebSocketOperationAn

export namespace ReflectControllerAnalyzer {
export interface IProps {
project: INestiaProject;
project: Omit<INestiaProject, "config">;
controller: INestiaSdkInput.IController;
unique: WeakSet<Function>;
}
Expand Down
2 changes: 1 addition & 1 deletion packages/sdk/src/analyses/ReflectHttpOperationAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import { ReflectMetadataAnalyzer } from "./ReflectMetadataAnalyzer";

export namespace ReflectHttpOperationAnalyzer {
export interface IProps {
project: INestiaProject;
project: Omit<INestiaProject, "config">;
controller: IReflectController;
function: Function;
name: string;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { ReflectMetadataAnalyzer } from "./ReflectMetadataAnalyzer";

export namespace ReflectWebSocketOperationAnalyzer {
export interface IProps {
project: INestiaProject;
project: Omit<INestiaProject, "config">;
controller: IReflectController;
function: Function;
name: string;
Expand Down
6 changes: 3 additions & 3 deletions packages/sdk/src/generates/SwaggerGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@ export namespace SwaggerGenerator {
};

export const compose = (props: {
config: INestiaConfig.ISwaggerConfig;
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
routes: ITypedHttpRoute[];
document: OpenApi.IDocument;
}): OpenApi.IDocument => {
Expand Down Expand Up @@ -100,7 +100,7 @@ export namespace SwaggerGenerator {
};

export const initialize = async (
config: INestiaConfig.ISwaggerConfig,
config: Omit<INestiaConfig.ISwaggerConfig, "output">,
): Promise<OpenApi.IDocument> => {
const pack = new Singleton(
async (): Promise<Partial<OpenApi.IDocument.IInfo> | null> => {
Expand Down Expand Up @@ -178,7 +178,7 @@ export namespace SwaggerGenerator {
};

const fillPaths = (props: {
config: INestiaConfig.ISwaggerConfig;
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
document: OpenApi.IDocument;
schema: (metadata: Metadata) => OpenApi.IJsonSchema | undefined;
routes: ITypedHttpRoute[];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { SwaggerOperationResponseComposer } from "./SwaggerOperationResponseComp

export namespace SwaggerOperationComposer {
export const compose = (props: {
config: INestiaConfig.ISwaggerConfig;
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
document: OpenApi.IDocument;
schema: (metadata: Metadata) => OpenApi.IJsonSchema | undefined;
route: ITypedHttpRoute;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { SwaggerDescriptionComposer } from "./SwaggerDescriptionComposer";

export namespace SwaggerOperationParameterComposer {
export interface IProps<Parameter extends ITypedHttpRouteParameter> {
config: INestiaConfig.ISwaggerConfig;
config: Omit<INestiaConfig.ISwaggerConfig, "output">;
document: OpenApi.IDocument;
schema: OpenApi.IJsonSchema;
jsDocTags: IJsDocTagInfo[];
Expand Down
1 change: 1 addition & 0 deletions packages/sdk/src/module.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
export * from "./INestiaConfig";
export * from "./NestiaSdkApplication";
export * from "./NestiaSwaggerComposer";
2 changes: 1 addition & 1 deletion packages/sdk/src/utils/VersioningStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export namespace VersioningStrategy {
value === undefined ? [] : Array.isArray(value) ? value : [value];

export const merge =
(project: INestiaProject) =>
(project: Omit<INestiaProject, "config">) =>
(values: Array<string | typeof VERSION_NEUTRAL>): string[] => {
if (project.input.versioning === undefined) return [""];
const set: Set<string | typeof VERSION_NEUTRAL> = new Set(values);
Expand Down
Loading

0 comments on commit 8ac3f83

Please sign in to comment.