448 lines
19 KiB
JavaScript
448 lines
19 KiB
JavaScript
"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<object | Uint8Array>;
|
|
}
|
|
|
|
${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<T extends ${TwirpContext} = ${TwirpContext}> {
|
|
${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<T extends ${TwirpContext} = ${TwirpContext}>(service: ${importService}Twirp<T>) {
|
|
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}<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>[]) => {
|
|
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<T extends ${TwirpContext} = ${TwirpContext}>(method: string, events: ${RouterEvents}<T>) {
|
|
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<T extends ${TwirpContext} = ${TwirpContext}>(ctx: T, service: ${service.name}Twirp ,data: Buffer, interceptors?: ${Interceptor}<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>[]): Promise<string | Uint8Array> {
|
|
switch (ctx.contentType) {
|
|
case ${TwirpContentType}.JSON:
|
|
return handle${formatMethodName(ctx, method.name, service.name)}JSON<T>(ctx, service, data, interceptors);
|
|
case ${TwirpContentType}.Protobuf:
|
|
return handle${formatMethodName(ctx, method.name, service.name)}Protobuf<T>(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<T extends ${TwirpContext} = ${TwirpContext}>(ctx: T, service: ${service.name}Twirp, data: Buffer, interceptors?: ${Interceptor}<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>[]) {
|
|
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<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>
|
|
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<T extends ${TwirpContext} = ${TwirpContext}>(ctx: T, service: ${service.name}Twirp, data: Buffer, interceptors?: ${Interceptor}<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>[]) {
|
|
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<T, ${relativeMessageName(ctx, file, method.inputType)}, ${relativeMessageName(ctx, file, method.outputType)}>
|
|
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;
|
|
}
|