Skip to content

Commit

Permalink
Close #887: SDK generator for @WebSocketRoute()
Browse files Browse the repository at this point in the history
  • Loading branch information
samchon committed Apr 29, 2024
1 parent f731eb4 commit b846eba
Show file tree
Hide file tree
Showing 19 changed files with 999 additions and 230 deletions.
31 changes: 11 additions & 20 deletions packages/sdk/src/NestiaSdkApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,12 @@ export class NestiaSdkApplication {
await validate("e2e")(this.config.e2e);

print_title("Nestia E2E Generator");
await this.generate(
"e2e",
(config) => config,
(checker) => (config) => async (routes) => {
await SdkGenerator.generate(checker)(config)(routes);
await E2eGenerator.generate(checker)(config)(routes);
},
);
await this.generate("e2e", (project) => async (routes) => {
await SdkGenerator.generate(project)(routes);
await E2eGenerator.generate(project)(
routes.filter((r) => r.protocol === "http") as ITypedHttpRoute[],
);
});
}

public async sdk(): Promise<void> {
Expand All @@ -71,7 +69,7 @@ export class NestiaSdkApplication {
);

print_title("Nestia SDK Generator");
await this.generate("sdk", (config) => config, SdkGenerator.generate);
await this.generate("sdk", SdkGenerator.generate);
}

public async swagger(): Promise<void> {
Expand All @@ -91,20 +89,13 @@ export class NestiaSdkApplication {
);

print_title("Nestia Swagger Generator");
await this.generate(
"swagger",
(config) => config.swagger!,
SwaggerGenerator.generate,
);
await this.generate("swagger", SwaggerGenerator.generate);
}

private async generate<Config>(
private async generate(
method: string,
config: (entire: INestiaConfig) => Config,
archiver: (
checker: ts.TypeChecker,
) => (
config: Config,
project: INestiaProject,
) => (
routes: Array<ITypedHttpRoute | ITypedWebSocketRoute>,
) => Promise<void>,
Expand Down Expand Up @@ -205,7 +196,7 @@ export class NestiaSdkApplication {

// DO GENERATE
AccessorAnalyzer.analyze(routeList);
await archiver(project.checker)(config(this.config))(routeList);
await archiver(project)(routeList);
}
}

Expand Down
1 change: 0 additions & 1 deletion packages/sdk/src/analyses/TypedHttpOperationAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -242,7 +242,6 @@ export namespace TypedHttpOperationAnalyzer {
props.symbol.valueDeclaration!,
);
const name: string = props.symbol.getEscapedName().toString();

const optional: boolean = !!project.checker.symbolToParameterDeclaration(
props.symbol,
undefined,
Expand Down
230 changes: 228 additions & 2 deletions packages/sdk/src/analyses/TypedWebSocketOperationAnalyzer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ import { HashMap } from "tstl";
import ts from "typescript";
import { CommentFactory } from "typia/lib/factories/CommentFactory";

import { IErrorReport } from "../structures/IErrorReport";
import { INestiaProject } from "../structures/INestiaProject";
import { IReflectController } from "../structures/IReflectController";
import { IReflectWebSocketOperation } from "../structures/IReflectWebSocketOperation";
import { ITypeTuple } from "../structures/ITypeTuple";
import { ITypedWebSocketRoute } from "../structures/ITypedWebSocketRoute";
import { PathUtil } from "../utils/PathUtil";
import { VersioningStrategy } from "../utils/VersioningStrategy";
Expand Down Expand Up @@ -46,10 +48,14 @@ export namespace TypedWebSocketOperationAnalyzer {

// EXPLORE CHILDREN TYPES
const importDict: ImportAnalyzer.Dictionary = new HashMap();
const errors: IErrorReport[] = [];
const parameters: Array<ITypedWebSocketRoute.IParameter> =
props.operation.parameters.map(
(param) =>
_Analyze_parameter(project)({
_Analyze_parameter({
...project,
errors,
})({
generics: props.generics,
imports: importDict,
controller: props.controller,
Expand All @@ -58,6 +64,11 @@ export namespace TypedWebSocketOperationAnalyzer {
symbol: signature.getParameters()[param.index],
})!,
);
if (errors.length) {
project.errors.push(...errors);
return [];
}

const imports: [string, string[]][] = importDict
.toJSON()
.map((pair) => [pair.first, pair.second.toJSON()]);
Expand Down Expand Up @@ -132,5 +143,220 @@ export namespace TypedWebSocketOperationAnalyzer {
function: string;
parameter: IReflectWebSocketOperation.IParameter;
symbol: ts.Symbol;
}): ITypedWebSocketRoute.IParameter => {};
}): ITypedWebSocketRoute.IParameter => {
if (props.parameter.category === "acceptor")
return _Analyze_acceptor(project)(props);
else if (props.parameter.category === "driver")
return _Analyze_driver(project)(props);

const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
props.symbol,
props.symbol.valueDeclaration!,
);
const name: string = props.symbol.getEscapedName().toString();

// VALIDATIONS
const errors: IErrorReport[] = [];
const tuple: ITypeTuple | null = ImportAnalyzer.analyze(project.checker)({
generics: props.generics,
imports: props.imports,
type,
});
if (
tuple === null ||
tuple.typeName === "__type" ||
tuple.typeName === "__object"
)
errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `implicit (unnamed) parameter type from ${JSON.stringify(name)}.`,
});
_Check_optional({
...project,
errors,
})({
...props,
parameter: {
name,
symbol: props.symbol,
},
});
if (errors.length) {
project.errors.push(...errors);
return null!;
}
return {
...props.parameter,
category: props.parameter.category,
name,
type: tuple!.type,
typeName: tuple!.typeName,
};
};

const _Analyze_acceptor =
(project: INestiaProject) =>
(props: {
generics: GenericAnalyzer.Dictionary;
imports: ImportAnalyzer.Dictionary;
controller: IReflectController;
function: string;
parameter: IReflectWebSocketOperation.IParameter;
symbol: ts.Symbol;
}): ITypedWebSocketRoute.IAcceptorParameter => {
// VALIDATIONS
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
props.symbol,
props.symbol.valueDeclaration!,
);
const name: string = props.symbol.getEscapedName().toString();
const errors: IErrorReport[] = [];
_Check_optional({
...project,
errors,
})({
...props,
parameter: {
name,
symbol: props.symbol,
},
});
if (
type.aliasTypeArguments === undefined ||
type.aliasTypeArguments.length !== 3
)
errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `@WebSocketRoute.Acceptor() must have three type arguments of WebAcceptor<Header, Provider, Remote>`,
});
const [header, provider, remote] = ["header", "provider", "remote"].map(
(key, i) => {
const tuple: ITypeTuple | null = ImportAnalyzer.analyze(
project.checker,
)({
generics: props.generics,
imports: props.imports,
type: type.aliasTypeArguments![i],
});
if (tuple === null)
errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `unable to analyze the "${key}" argument type of WebAcceptor<Header, Provider, Remote>.`,
});
return tuple!;
},
);

if (errors.length) {
project.errors.push(...errors);
return null!;
}
return {
...props.parameter,
category: "acceptor",
name,
header,
provider,
remote,
};
};

const _Analyze_driver =
(project: INestiaProject) =>
(props: {
generics: GenericAnalyzer.Dictionary;
imports: ImportAnalyzer.Dictionary;
controller: IReflectController;
function: string;
parameter: IReflectWebSocketOperation.IParameter;
symbol: ts.Symbol;
}): ITypedWebSocketRoute.IDriverParameter => {
// VALIDATIONS
const type: ts.Type = project.checker.getTypeOfSymbolAtLocation(
props.symbol,
props.symbol.valueDeclaration!,
);
const name: string = props.symbol.getEscapedName().toString();
const errors: IErrorReport[] = [];
_Check_optional({
...project,
errors,
})({
...props,
parameter: {
name,
symbol: props.symbol,
},
});
const tuple: ITypeTuple = (() => {
if (type.aliasTypeArguments?.length !== 1) {
errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `@WebSocketRoute.Driver() must have one type argument of WebDriver<T>`,
});
return null!;
} else {
const tuple: ITypeTuple | null = ImportAnalyzer.analyze(
project.checker,
)({
generics: props.generics,
imports: props.imports,
type: type.aliasTypeArguments[0],
});
if (tuple === null)
errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `unable to analyze the "type" argument of WebDriver<T>.`,
});
return tuple!;
}
})();
if (errors.length) {
project.errors.push(...errors);
return null!;
}
return {
...props.parameter,
category: "driver",
name,
type: tuple.type,
typeName: tuple.typeName,
};
};

const _Check_optional =
(project: INestiaProject) =>
(props: {
controller: IReflectController;
function: string;
parameter: {
name: string;
symbol: ts.Symbol;
};
}) => {
const optional: boolean = !!project.checker.symbolToParameterDeclaration(
props.parameter.symbol,
undefined,
undefined,
)?.questionToken;
if (optional === true)
project.errors.push({
file: props.controller.file,
controller: props.controller.name,
function: props.function,
message: `@WebSocketRoute() does not allow optional parameter, but be detected from ${JSON.stringify(
props.parameter.symbol.getEscapedName().toString(),
)}.`,
});
};
}
28 changes: 15 additions & 13 deletions packages/sdk/src/generates/CloneGenerator.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,40 @@
import fs from "fs";
import ts from "typescript";

import { INestiaConfig } from "../INestiaConfig";
import { INestiaProject } from "../structures/INestiaProject";
import { ITypedHttpRoute } from "../structures/ITypedHttpRoute";
import { FilePrinter } from "./internal/FilePrinter";
import { ImportDictionary } from "./internal/ImportDictionary";
import { SdkCloneProgrammer } from "./internal/SdkCloneProgrammer";
import { SdkHttpCloneProgrammer } from "./internal/SdkHttpCloneProgrammer";

export namespace CloneGenerator {
export const write =
(checker: ts.TypeChecker) =>
(config: INestiaConfig) =>
(project: INestiaProject) =>
async (routes: ITypedHttpRoute[]): Promise<void> => {
const dict: Map<string, SdkCloneProgrammer.IModule> =
SdkCloneProgrammer.write(checker)(config)(routes);
const dict: Map<string, SdkHttpCloneProgrammer.IModule> =
SdkHttpCloneProgrammer.write(project)(routes);
if (dict.size === 0) return;
try {
await fs.promises.mkdir(`${config.output}/structures`);
await fs.promises.mkdir(`${project.config.output}/structures`);
} catch {}
for (const [key, value] of dict) await writeDtoFile(config)(key, value);
for (const [key, value] of dict) await writeDtoFile(project)(key, value);
};

const writeDtoFile =
(config: INestiaConfig) =>
async (key: string, value: SdkCloneProgrammer.IModule): Promise<void> => {
const location: string = `${config.output}/structures/${key}.ts`;
(project: INestiaProject) =>
async (
key: string,
value: SdkHttpCloneProgrammer.IModule,
): Promise<void> => {
const location: string = `${project.config.output}/structures/${key}.ts`;
const importer: ImportDictionary = new ImportDictionary(location);
const statements: ts.Statement[] = iterate(importer)(value);
if (statements.length === 0) return;

await FilePrinter.write({
location,
statements: [
...importer.toStatements(`${config.output}/structures`),
...importer.toStatements(`${project.config.output}/structures`),
...(importer.empty() ? [] : [FilePrinter.enter()]),
...statements,
],
Expand All @@ -41,7 +43,7 @@ export namespace CloneGenerator {

const iterate =
(importer: ImportDictionary) =>
(modulo: SdkCloneProgrammer.IModule): ts.Statement[] => {
(modulo: SdkHttpCloneProgrammer.IModule): ts.Statement[] => {
const output: ts.Statement[] = [];
if (modulo.programmer !== null) output.push(modulo.programmer(importer));
if (modulo.children.size) {
Expand Down
Loading

0 comments on commit b846eba

Please sign in to comment.