"use strict"; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.generateTwirpServer = exports.generateTwirpClient = exports.generateTwirp = void 0; const ts_poet_1 = require("ts-poet"); const camel_case_1 = require("camel-case"); const local_type_name_1 = require("../local-type-name"); const path_1 = __importDefault(require("path")); const TwirpServer = ts_poet_1.imp("TwirpServer@twirp-ts"); const Interceptor = ts_poet_1.imp("Interceptor@twirp-ts"); const RouterEvents = ts_poet_1.imp("RouterEvents@twirp-ts"); const chainInterceptors = ts_poet_1.imp("chainInterceptors@twirp-ts"); const TwirpContentType = ts_poet_1.imp("TwirpContentType@twirp-ts"); const TwirpContext = ts_poet_1.imp("TwirpContext@twirp-ts"); const TwirpError = ts_poet_1.imp("TwirpError@twirp-ts"); const TwirpErrorCode = ts_poet_1.imp("TwirpErrorCode@twirp-ts"); /** * Generates the client and server implementation of the twirp * specification. * @param ctx * @param file */ function generateTwirp(ctx, file) { return __awaiter(this, void 0, void 0, function* () { const contents = file.service.map((service) => { return ts_poet_1.joinCode([ genClient(ctx, file, service), genServer(ctx, file, service), ], { on: "\n\n" }); }); return ts_poet_1.joinCode(contents, { on: "\n\n" }).toStringWithImports(); }); } exports.generateTwirp = generateTwirp; /** * Generates the client implementation of the twirp specification. * @param ctx * @param file */ function generateTwirpClient(ctx, file) { return __awaiter(this, void 0, void 0, function* () { const contents = file.service.map((service) => { return ts_poet_1.joinCode([genClient(ctx, file, service)], { on: "\n\n" }); }); return ts_poet_1.joinCode(contents, { on: "\n\n" }).toStringWithImports(); }); } exports.generateTwirpClient = generateTwirpClient; /** * Generates the server implementation of the twirp specification. * @param ctx * @param file */ function generateTwirpServer(ctx, file) { return __awaiter(this, void 0, void 0, function* () { const contents = file.service.map((service) => { return ts_poet_1.joinCode([genServer(ctx, file, service)], { on: "\n\n" }); }); return ts_poet_1.joinCode(contents, { on: "\n\n" }).toStringWithImports(); }); } exports.generateTwirpServer = generateTwirpServer; function genClient(ctx, file, service) { return ts_poet_1.code ` //==================================// // Client Code // //==================================// interface Rpc { request( service: string, method: string, contentType: "application/json" | "application/protobuf", data: object | Uint8Array, ): Promise; } ${genTwirpClientInterface(ctx, file, service)} ${genTwripClientJSONImpl(ctx, file, service)} ${genTwripClientProtobufImpl(ctx, file, service)} `; } function genTwirpClientInterface(ctx, file, service) { const methods = service.method.map((method) => { return ts_poet_1.code ` ${formatMethodName(ctx, method.name)}(request: ${relativeMessageName(ctx, file, method.inputType)}): Promise<${relativeMessageName(ctx, file, method.outputType)}> `; }); return ts_poet_1.code ` export interface ${service.name}Client { ${ts_poet_1.joinCode(methods, { on: "\n" })} } `; } /** * Generates the json client * @param ctx * @param file * @param service */ function genTwripClientJSONImpl(ctx, file, service) { const methods = service.method.map((method) => { return ts_poet_1.code ` ${formatMethodName(ctx, method.name)}(request: ${relativeMessageName(ctx, file, method.inputType)}): Promise<${relativeMessageName(ctx, file, method.outputType)}> { const data = ${relativeMessageName(ctx, file, method.inputType)}.${encodeJSON(ctx, "request")}; const promise = this.rpc.request( "${file.package ? file.package + "." : ""}${service.name}", "${formatMethodName(ctx, method.name)}", "application/json", data as object, ); return promise.then((data) => ${relativeMessageName(ctx, file, method.outputType)}.${decodeJSON(ctx, "data as any")}); } `; }); const bindings = service.method.map((method) => { return ts_poet_1.code ` this.${formatMethodName(ctx, method.name)}.bind(this); `; }); return ts_poet_1.code ` export class ${service.name}ClientJSON implements ${service.name}Client { private readonly rpc: Rpc; constructor(rpc: Rpc) { this.rpc = rpc; ${ts_poet_1.joinCode(bindings, { on: `\n` })} } ${ts_poet_1.joinCode(methods, { on: `\n\n` })} } `; } /** * Generate the protobuf client * @param ctx * @param file * @param service */ function genTwripClientProtobufImpl(ctx, file, service) { const methods = service.method.map((method) => { return ts_poet_1.code ` ${formatMethodName(ctx, method.name)}(request: ${relativeMessageName(ctx, file, method.inputType)}): Promise<${relativeMessageName(ctx, file, method.outputType)}> { const data = ${relativeMessageName(ctx, file, method.inputType)}.${encodeProtobuf(ctx, "request")}; const promise = this.rpc.request( "${file.package ? file.package + "." : ""}${service.name}", "${formatMethodName(ctx, method.name)}", "application/protobuf", data, ); return promise.then((data) => ${relativeMessageName(ctx, file, method.outputType)}.${decodeProtobuf(ctx, "data as Uint8Array")}); } `; }); const bindings = service.method.map((method) => { return ts_poet_1.code ` this.${formatMethodName(ctx, method.name)}.bind(this); `; }); return ts_poet_1.code ` export class ${service.name}ClientProtobuf implements ${service.name}Client { private readonly rpc: Rpc; constructor(rpc: Rpc) { this.rpc = rpc; ${ts_poet_1.joinCode(bindings, { on: `\n` })} } ${ts_poet_1.joinCode(methods, { on: `\n\n` })} } `; } /** * Generates twirp service definition * @param ctx * @param file * @param service */ function genTwirpService(ctx, file, service) { const importService = service.name; const serverMethods = service.method.map((method) => { return ts_poet_1.code ` ${formatMethodName(ctx, method.name)}(ctx: T, request: ${relativeMessageName(ctx, file, method.inputType)}): Promise<${relativeMessageName(ctx, file, method.outputType)}> `; }); const methodEnum = service.method.map((method) => { return ts_poet_1.code `${formatMethodName(ctx, method.name)} = "${formatMethodName(ctx, method.name)}",`; }); const methodList = service.method.map((method) => { return ts_poet_1.code `${importService}Method.${formatMethodName(ctx, method.name)}`; }); return ts_poet_1.code ` export interface ${importService}Twirp { ${ts_poet_1.joinCode(serverMethods, { on: `\n` })} } export enum ${importService}Method { ${ts_poet_1.joinCode(methodEnum, { on: "\n" })} } export const ${importService}MethodList = [${ts_poet_1.joinCode(methodList, { on: "," })}]; `; } /** * Generates the twirp server specification * @param ctx * @param file * @param service */ function genServer(ctx, file, service) { var _a; const importService = service.name; return ts_poet_1.code ` //==================================// // Server Code // //==================================// ${genTwirpService(ctx, file, service)} export function create${importService}Server(service: ${importService}Twirp) { return new ${TwirpServer}<${importService}Twirp, T>({ service, packageName: "${(_a = file.package) !== null && _a !== void 0 ? _a : ''}", serviceName: "${importService}", methodList: ${importService}MethodList, matchRoute: match${importService}Route, }) } ${genRouteHandler(ctx, file, service)} ${ts_poet_1.joinCode(genHandleRequestMethod(ctx, file, service), { on: "\n\n" })} ${ts_poet_1.joinCode(genHandleJSONRequest(ctx, file, service), { on: "\n\n" })} ${ts_poet_1.joinCode(genHandleProtobufRequest(ctx, file, service), { on: "\n\n" })} `; } /** * Generate the route handler * @param ctx * @param file * @param service */ function genRouteHandler(ctx, file, service) { const cases = service.method.map(method => ts_poet_1.code ` case "${formatMethodName(ctx, method.name)}": return async (ctx: T, service: ${service.name}Twirp ,data: Buffer, interceptors?: ${Interceptor}[]) => { ctx = {...ctx, methodName: "${formatMethodName(ctx, method.name)}" } await events.onMatch(ctx); return handle${formatMethodName(ctx, method.name, service.name)}Request(ctx, service, data, interceptors) } `); return ts_poet_1.code ` function match${service.name}Route(method: string, events: ${RouterEvents}) { switch(method) { ${ts_poet_1.joinCode(cases, { on: `\n` })} default: events.onNotFound(); const msg = \`no handler found\`; throw new ${TwirpError}(${TwirpErrorCode}.BadRoute, msg) } } `; } /** * Generate request handler for methods * @param ctx * @param file * @param service */ function genHandleRequestMethod(ctx, file, service) { return service.method.map(method => { return ts_poet_1.code ` function handle${formatMethodName(ctx, method.name, service.name)}Request(ctx: T, service: ${service.name}Twirp ,data: Buffer, interceptors?: ${Interceptor}[]): Promise { switch (ctx.contentType) { case ${TwirpContentType}.JSON: return handle${formatMethodName(ctx, method.name, service.name)}JSON(ctx, service, data, interceptors); case ${TwirpContentType}.Protobuf: return handle${formatMethodName(ctx, method.name, service.name)}Protobuf(ctx, service, data, interceptors); default: const msg = "unexpected Content-Type"; throw new ${TwirpError}(${TwirpErrorCode}.BadRoute, msg); } } `; }); } /** * Generate a JSON request handler for a method * @param ctx * @param file * @param service */ function genHandleJSONRequest(ctx, file, service) { return service.method.map(method => { return ts_poet_1.code ` async function handle${formatMethodName(ctx, method.name, service.name)}JSON(ctx: T, service: ${service.name}Twirp, data: Buffer, interceptors?: ${Interceptor}[]) { let request: ${relativeMessageName(ctx, file, method.inputType)} let response: ${relativeMessageName(ctx, file, method.outputType)} try { const body = JSON.parse(data.toString() || "{}"); request = ${relativeMessageName(ctx, file, method.inputType)}.${decodeJSON(ctx, "body")}; } catch(e) { if (e instanceof Error) { const msg = "the json request could not be decoded"; throw new ${TwirpError}(${TwirpErrorCode}.Malformed, msg).withCause(e, true); } } if (interceptors && interceptors.length > 0) { const interceptor = ${chainInterceptors}(...interceptors) as Interceptor response = await interceptor(ctx, request!, (ctx, inputReq) => { return service.${formatMethodName(ctx, method.name)}(ctx, inputReq); }); } else { response = await service.${formatMethodName(ctx, method.name)}(ctx, request!) } return JSON.stringify(${relativeMessageName(ctx, file, method.outputType)}.${encodeJSON(ctx, "response")} as string); } `; }); } /** * Generates a protobuf request handler * @param ctx * @param file * @param service */ function genHandleProtobufRequest(ctx, file, service) { return service.method.map(method => { return ts_poet_1.code ` async function handle${formatMethodName(ctx, method.name, service.name)}Protobuf(ctx: T, service: ${service.name}Twirp, data: Buffer, interceptors?: ${Interceptor}[]) { let request: ${relativeMessageName(ctx, file, method.inputType)} let response: ${relativeMessageName(ctx, file, method.outputType)} try { request = ${relativeMessageName(ctx, file, method.inputType)}.${decodeProtobuf(ctx, "data")}; } catch(e) { if (e instanceof Error) { const msg = "the protobuf request could not be decoded"; throw new ${TwirpError}(${TwirpErrorCode}.Malformed, msg).withCause(e, true); } } if (interceptors && interceptors.length > 0) { const interceptor = ${chainInterceptors}(...interceptors) as Interceptor response = await interceptor(ctx, request!, (ctx, inputReq) => { return service.${formatMethodName(ctx, method.name)}(ctx, inputReq); }); } else { response = await service.${formatMethodName(ctx, method.name)}(ctx, request!) } return Buffer.from(${relativeMessageName(ctx, file, method.outputType)}.${encodeProtobuf(ctx, "response")}); } `; }); } var SupportedLibs; (function (SupportedLibs) { SupportedLibs["TSProto"] = "ts-proto"; SupportedLibs["ProtobufTS"] = "protobuf-ts"; })(SupportedLibs || (SupportedLibs = {})); function validateLib(lib) { switch (lib) { case "ts-proto": return SupportedLibs.TSProto; case "protobuf-ts": return SupportedLibs.ProtobufTS; default: throw new Error(`library ${lib} not supported`); } } function decodeJSON(ctx, dataName) { const protoLib = validateLib(ctx.lib); if (protoLib === SupportedLibs.TSProto) { return ts_poet_1.code `fromJSON(${dataName})`; } return ts_poet_1.code `fromJson(${dataName}, { ignoreUnknownFields: true })`; } function encodeJSON(ctx, dataName) { const protoLib = validateLib(ctx.lib); if (protoLib === SupportedLibs.TSProto) { return ts_poet_1.code `toJSON(${dataName})`; } return ts_poet_1.code `toJson(${dataName}, {useProtoFieldName: true, emitDefaultValues: ${ctx.emitDefaultValues ? 'true' : 'false'}})`; } function encodeProtobuf(ctx, dataName) { const protoLib = validateLib(ctx.lib); if (protoLib === SupportedLibs.TSProto) { return ts_poet_1.code `encode(${dataName}).finish()`; } return ts_poet_1.code `toBinary(${dataName})`; } function decodeProtobuf(ctx, dataName) { const protoLib = validateLib(ctx.lib); if (protoLib === SupportedLibs.TSProto) { return ts_poet_1.code `decode(${dataName})`; } return ts_poet_1.code `fromBinary(${dataName})`; } function relativeMessageName(ctx, file, messageName) { const registry = ctx.registry; const symbols = ctx.symbols; const entry = symbols.find(registry.resolveTypeName(messageName)); if (!entry) { throw new Error(`Message ${messageName} not found`); } const messageType = local_type_name_1.createLocalTypeName(entry.descriptor, registry); const relativePath = createRelativeImportPath(file.name, entry.file.getFilename()); return ts_poet_1.code `${ts_poet_1.imp(`${messageType}@${relativePath}`)}`; } /** * Create a relative path for an import statement like * `import {Foo} from "./foo"` */ function createRelativeImportPath(currentPath, pathToImportFrom) { // create relative path to the file to import let fromPath = path_1.default.relative(path_1.default.dirname(currentPath), pathToImportFrom); // on windows, this may add backslash directory separators. // we replace them with forward slash. if (path_1.default.sep !== "/") { fromPath = fromPath.split(path_1.default.sep).join("/"); } // drop file extension fromPath = fromPath.replace(/\.[a-z]+$/, ''); // make sure to start with './' to signal relative path to module resolution if (!fromPath.startsWith('../') && !fromPath.startsWith('./')) { fromPath = './' + fromPath; } return fromPath; } function formatMethodName(ctx, methodName, serviceName) { if (methodName === undefined) return undefined; serviceName = serviceName || ""; return ctx.camelCase ? camel_case_1.camelCase(serviceName) + camel_case_1.camelCase(methodName) : serviceName + methodName; }