UNPKG

@nestia/sdk

Version:

Nestia SDK and Swagger generator

308 lines (281 loc) 10.3 kB
import fs from "fs"; import path from "path"; import { HashSet, Pair, 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 { TypedWebSocketRouteAnalyzer } from "./analyses/TypedWebSocketRouteAnalyzer"; import { E2eGenerator } from "./generates/E2eGenerator"; import { SdkGenerator } from "./generates/SdkGenerator"; import { SwaggerGenerator } from "./generates/SwaggerGenerator"; import { INestiaProject } from "./structures/INestiaProject"; import { IReflectController } from "./structures/IReflectController"; import { IReflectOperationError } from "./structures/IReflectOperationError"; import { ITypedApplication } from "./structures/ITypedApplication"; import { ITypedHttpRoute } from "./structures/ITypedHttpRoute"; import { ITypedWebSocketRoute } from "./structures/ITypedWebSocketRoute"; import { IOperationMetadata } from "./transformers/IOperationMetadata"; import { StringUtil } from "./utils/StringUtil"; import { VersioningStrategy } from "./utils/VersioningStrategy"; export class NestiaSdkApplication { public constructor(private readonly config: INestiaConfig) {} public async all(): Promise<void> { if (!this.config.output && !this.config.swagger?.output) throw new Error( [ "Error on NestiaApplication.all(): nothing to generate, configure at least one property of below:", "", " - INestiaConfig.output", " - INestiaConfig.swagger.output", " - INestiaConfig.openai.output", ].join("\n"), ); print_title("Nestia All Generator"); await this.generate({ generate: async (app) => { if (this.config.output) { await SdkGenerator.generate(app); if (this.config.e2e) await E2eGenerator.generate(app); } if (this.config.swagger) await SwaggerGenerator.generate(app); }, validate: this.config.output ? SdkGenerator.validate : undefined, }); } public async e2e(): Promise<void> { if (!this.config.output) throw new Error( "Error on NestiaApplication.e2e(): configure INestiaConfig.output property.", ); else if (!this.config.e2e) throw new Error( "Error on NestiaApplication.e2e(): configure INestiaConfig.e2e property.", ); const validate = (title: string) => async (location: string): Promise<void> => { const parent: string = path.resolve(location + "/.."); const stats: fs.Stats = await fs.promises.lstat(parent); if (stats.isDirectory() === false) throw new Error( `Error on NestiaApplication.e2e(): output directory of ${title} does not exists.`, ); }; await validate("sdk")(this.config.output); await validate("e2e")(this.config.e2e); print_title("Nestia E2E Generator"); await this.generate({ generate: async (app) => { await SdkGenerator.generate(app); await E2eGenerator.generate(app); }, }); } public async sdk(): Promise<void> { if (!this.config.output) throw new Error( "Error on NestiaApplication.sdk(): configure INestiaConfig.output property.", ); const parent: string = path.resolve(this.config.output + "/.."); const stats: fs.Stats = await fs.promises.lstat(parent); if (stats.isDirectory() === false) throw new Error( "Error on NestiaApplication.sdk(): output directory does not exists.", ); print_title("Nestia SDK Generator"); await this.generate({ generate: SdkGenerator.generate, validate: SdkGenerator.validate, }); } public async swagger(): Promise<void> { if (!this.config.swagger?.output) throw new Error( `Error on NestiaApplication.swagger(): configure INestiaConfig.swagger property.`, ); const parsed: path.ParsedPath = path.parse(this.config.swagger.output); const directory: string = !!parsed.ext ? path.resolve(parsed.dir) : this.config.swagger.output; const stats: fs.Stats = await fs.promises.lstat(directory); if (stats.isDirectory() === false) throw new Error( "Error on NestiaApplication.swagger(): output directory does not exists.", ); print_title("Nestia Swagger Generator"); await this.generate({ generate: SwaggerGenerator.generate, }); } private async generate(props: { generate: (app: ITypedApplication) => Promise<void>; validate?: (app: ITypedApplication) => IReflectOperationError[]; }): Promise<void> { //---- // ANALYZE REFLECTS //---- const unique: WeakSet<any> = new WeakSet(); const project: INestiaProject = { config: this.config, input: await ConfigAnalyzer.input(this.config), checker: null!, errors: [], warnings: [], }; console.log("Analyzing reflections"); const controllers: IReflectController[] = project.input.controllers .map((c) => ReflectControllerAnalyzer.analyze({ project, controller: c, unique }), ) .filter((c): c is IReflectController => c !== null); if (project.warnings.length) report({ type: "warning", errors: project.warnings, }); if (project.errors.length) return report({ type: "error", errors: project.errors, }); const agg: number = (() => { const set: HashSet<Pair<string, string>> = new HashSet(); for (const controller of controllers) for (const controllerPath of controller.paths) for (const operation of controller.operations) for (const operationPath of operation.paths) set.insert( new Pair( `${controllerPath}/${operationPath}`, operation.protocol === "http" ? operation.method : "", ), ); return set.size(); })(); console.log(` - controllers: #${controllers.length}`); console.log(` - paths: #${agg}`); console.log( ` - routes: #${controllers .map( (c) => c.paths.length * c.operations.map((f) => f.paths.length).reduce((a, b) => a + b, 0), ) .reduce((a, b) => a + b, 0)}`, ); //---- // ANALYZE TYPESCRIPT CODE //---- console.log("Analyzing source codes"); // METADATA COMPONENTS const collection: IMetadataDictionary = TypedHttpRouteAnalyzer.dictionary(controllers); // CONVERT TO TYPED OPERATIONS const globalPrefix: string = project.input.globalPrefix?.prefix ?? ""; const routes: Array<ITypedHttpRoute | ITypedWebSocketRoute> = []; 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), }), ); else if (o.protocol === "websocket") routes.push( ...TypedWebSocketRouteAnalyzer.analyze({ controller: c, operation: o, paths: Array.from(pathList), }), ); } AccessorAnalyzer.analyze(routes); if (props.validate !== undefined) project.errors.push( ...props.validate({ project, collection, routes, }), ); if (project.errors.length) return report({ type: "error", errors: project.errors, }); await props.generate({ project, collection, routes, }); } } const print_title = (str: string): void => { console.log("-----------------------------------------------------------"); console.log(` ${str}`); console.log("-----------------------------------------------------------"); }; 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); console.log(""); print_title(`Nestia ${StringUtil.capitalize(props.type)} Report`); for (const { first: { error }, second: contents, } of map) { if (error.contents.length === 0) continue; const location: string = path.relative(process.cwd(), error.file); const message: string = [ `${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(""); if (props.type === "error") throw new Error(message); else console.log(message); } }; const wrapPaths = (paths: string[]): string[] => paths.length === 0 ? [""] : paths;