initial commit of actions

This commit is contained in:
Dominik Polakovics Polakovics 2026-01-31 18:56:04 +01:00
commit 949ece5785
44660 changed files with 12034344 additions and 0 deletions

View file

@ -0,0 +1,9 @@
import { GeneratedFile } from "@protobuf-ts/plugin-framework";
export declare class File implements GeneratedFile {
readonly fileName: string;
private content;
constructor(fileName: string);
getFilename(): string;
setContent(content: string): this;
getContent(): string;
}

View file

@ -0,0 +1,20 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.File = void 0;
class File {
constructor(fileName) {
this.fileName = fileName;
this.content = "";
}
getFilename() {
return this.fileName;
}
setContent(content) {
this.content = content;
return this;
}
getContent() {
return this.content;
}
}
exports.File = File;

View file

@ -0,0 +1,30 @@
import { FileDescriptorProto } from "@protobuf-ts/plugin-framework";
import { MatchFunction } from "path-to-regexp";
export declare enum Pattern {
POST = "post",
GET = "get",
PATCH = "patch",
PUT = "put",
DELETE = "delete"
}
export interface HttpRoute {
serviceName: string;
methodName: string;
packageName: string;
matchingPath: string;
matcher: MatchFunction;
httpMethod: Pattern;
bodyKey?: string;
responseBodyKey?: string;
additionalBindings?: HttpRoute;
}
export declare type HttpRulePattern = {
[key in Pattern]: string;
};
export interface HttpOption extends HttpRulePattern {
body: string;
responseBody: string;
additional_bindings: HttpOption;
}
export declare function genGateway(ctx: any, files: readonly FileDescriptorProto[]): Promise<string>;
export declare function getMethod(httpSpec: HttpOption): Pattern;

View file

@ -0,0 +1,113 @@
"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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.getMethod = exports.genGateway = exports.Pattern = void 0;
const fs_1 = require("fs");
const ts_poet_1 = require("ts-poet");
const path_to_regexp_1 = require("path-to-regexp");
const Gateway = ts_poet_1.imp("Gateway@twirp-ts");
const GatewayPattern = ts_poet_1.imp("Pattern@twirp-ts");
const pathToRegexpMatch = ts_poet_1.imp("match@path-to-regexp");
const debug = (content) => fs_1.writeFileSync(__dirname + "/debug.json", JSON.stringify(content, null, 2), "utf-8");
var Pattern;
(function (Pattern) {
Pattern["POST"] = "post";
Pattern["GET"] = "get";
Pattern["PATCH"] = "patch";
Pattern["PUT"] = "put";
Pattern["DELETE"] = "delete";
})(Pattern = exports.Pattern || (exports.Pattern = {}));
function genGateway(ctx, files) {
return __awaiter(this, void 0, void 0, function* () {
const httpRoutes = files.reduce((all, current) => {
current.service.forEach(service => {
service.method.forEach((method) => {
const options = ctx.interpreter.readOptions(method);
if (options && options["google.api.http"]) {
const httpSpec = options["google.api.http"];
all.push(parseHttpOption(httpSpec, current.package || "", method.name, service.name));
if (httpSpec.additional_bindings) {
all.push(parseHttpOption(httpSpec.additional_bindings, current.package || "", method.name, service.name));
}
}
});
});
return all;
}, []);
return genGatewayHandler(httpRoutes).toStringWithImports();
});
}
exports.genGateway = genGateway;
function genGatewayHandler(httpRoute) {
const genRoutes = (method) => httpRoute.filter(route => route.httpMethod === method).map(route => {
return ts_poet_1.code `
{
packageName: "${route.packageName}",
methodName: "${route.methodName}",
serviceName: "${route.serviceName}",
httpMethod: "${route.httpMethod}" as ${GatewayPattern},
matchingPath: "${route.matchingPath}{:query_string(\\\\?.*)}?",
matcher: ${pathToRegexpMatch}("${route.matchingPath}{:query_string(\\\\?.*)}?"),
bodyKey: "${route.bodyKey || ""}",
responseBodyKey: "${route.responseBodyKey || ""}",
},
`;
});
return ts_poet_1.code `
export function createGateway() {
return new ${Gateway}({
post: [${ts_poet_1.joinCode(genRoutes(Pattern.POST), { on: "\n" })}],
get: [${ts_poet_1.joinCode(genRoutes(Pattern.GET), { on: "\n" })}],
put: [${ts_poet_1.joinCode(genRoutes(Pattern.PUT), { on: "\n" })}],
patch: [${ts_poet_1.joinCode(genRoutes(Pattern.PATCH), { on: "\n" })}],
delete: [${ts_poet_1.joinCode(genRoutes(Pattern.DELETE), { on: "\n" })}],
})
}
`;
}
function parseHttpOption(httpOption, packageName, methodName, serviceName) {
const httpMethod = getMethod(httpOption);
const matchingUrl = httpOption[httpMethod];
const matchingPath = matcher(matchingUrl);
const httpRoute = {
packageName,
methodName,
serviceName,
httpMethod: httpMethod,
matchingPath,
matcher: path_to_regexp_1.match(matchingPath),
bodyKey: httpOption.body,
responseBodyKey: httpOption.responseBody,
};
return httpRoute;
}
function matcher(url) {
return url.split("/").map((urlSegment) => {
const matchURLParams = /{([0-9a-zA-Z_-]+)}/.exec(urlSegment);
if (matchURLParams && matchURLParams.length > 0) {
const paramName = matchURLParams[1];
return "{:" + paramName + "}";
}
else {
return urlSegment;
}
}).join("/");
}
function getMethod(httpSpec) {
const possibleMethods = ["post", "get", "patch", "put", "delete"];
for (const method of possibleMethods) {
if (method in httpSpec) {
return method;
}
}
throw new Error(`HTTP method not found`);
}
exports.getMethod = getMethod;

View file

@ -0,0 +1,3 @@
import { DescriptorRegistry } from "@protobuf-ts/plugin-framework";
import { File } from "../file";
export declare function genIndexFile(registry: DescriptorRegistry, files: File[]): File;

View file

@ -0,0 +1,29 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.genIndexFile = void 0;
const file_1 = require("../file");
function genIndexFile(registry, files) {
const fileToExport = registry.allFiles()
.filter((fileDescriptor) => {
let hasExports = false;
registry.visitTypes(fileDescriptor, descriptor => {
// we are not interested in synthetic types like map entry messages
if (registry.isSyntheticElement(descriptor))
return;
hasExports = true;
});
return hasExports;
})
.map((file => { var _a; return (_a = file.name) === null || _a === void 0 ? void 0 : _a.replace(".proto", ""); }));
const compiledFiles = files.filter(file => file.getContent() !== "").map(file => {
return file.fileName.replace(".ts", "");
});
if (compiledFiles.length > 0) {
fileToExport.push(...compiledFiles);
}
const indexFile = new file_1.File('index.ts');
return indexFile.setContent(fileToExport.map((fileName) => {
return `export * from "./${fileName}";`;
}).join("\n"));
}
exports.genIndexFile = genIndexFile;

View file

@ -0,0 +1,17 @@
import { FileDescriptorProto } from "@protobuf-ts/plugin-framework";
interface OpenAPIDoc {
fileName: string;
content: string;
}
export declare enum OpenAPIType {
GATEWAY = 0,
TWIRP = 1
}
/**
* Generate twirp compliant OpenAPI doc
* @param ctx
* @param files
* @param type
*/
export declare function genOpenAPI(ctx: any, files: readonly FileDescriptorProto[], type: OpenAPIType): Promise<OpenAPIDoc[]>;
export {};

View file

@ -0,0 +1,580 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.genOpenAPI = exports.OpenAPIType = void 0;
const plugin_framework_1 = require("@protobuf-ts/plugin-framework");
const yaml = __importStar(require("yaml"));
const local_type_name_1 = require("../local-type-name");
const gateway_1 = require("./gateway");
var OpenAPIType;
(function (OpenAPIType) {
OpenAPIType[OpenAPIType["GATEWAY"] = 0] = "GATEWAY";
OpenAPIType[OpenAPIType["TWIRP"] = 1] = "TWIRP";
})(OpenAPIType = exports.OpenAPIType || (exports.OpenAPIType = {}));
/**
* Generate twirp compliant OpenAPI doc
* @param ctx
* @param files
* @param type
*/
function genOpenAPI(ctx, files, type) {
return __awaiter(this, void 0, void 0, function* () {
const documents = [];
files.forEach(file => {
file.service.forEach((service) => {
var _a, _b;
const document = {
openapi: "3.0.3",
info: {
title: `${service.name}`,
version: "1.0.0",
description: genDescription(ctx, service),
},
paths: type === OpenAPIType.TWIRP ?
genTwirpPaths(ctx, file, service) :
genGatewayPaths(ctx, file, service),
components: genComponents(ctx, service.method),
};
const fileName = type === OpenAPIType.TWIRP ?
`${(_a = service.name) === null || _a === void 0 ? void 0 : _a.toLowerCase()}.twirp.openapi.yaml` :
`${(_b = service.name) === null || _b === void 0 ? void 0 : _b.toLowerCase()}.openapi.yaml`;
documents.push({
fileName,
content: yaml.stringify(document),
});
});
});
return documents;
});
}
exports.genOpenAPI = genOpenAPI;
/**
* Generates OpenAPI Twirp URI paths
* @param ctx
* @param file
* @param service
*/
function genTwirpPaths(ctx, file, service) {
return service.method.reduce((paths, method) => {
const description = genDescription(ctx, method);
paths[`/${file.package ? file.package + "." : ""}${service.name}/${method.name}`] = {
post: {
summary: description,
operationId: `${service.name}_${method.name}`,
requestBody: {
required: true,
content: {
"application/json": {
schema: {
$ref: genRef(ctx, method.inputType)
}
}
}
},
responses: {
"200": {
description: "OK",
content: {
"application/json": {
schema: {
$ref: genRef(ctx, method.outputType),
}
}
}
}
}
}
};
return paths;
}, {});
}
/**
* Generates OpenAPI Twrip Gateway URI paths
* @param ctx
* @param file
* @param service
*/
function genGatewayPaths(ctx, file, service) {
const registry = ctx.registry;
/**
* Build paths recursively
* @param method
* @param httpSpec
* @param paths
*/
function buildPath(method, httpSpec, paths) {
const httpMethod = gateway_1.getMethod(httpSpec);
const description = genDescription(ctx, method);
const pathItem = {
[httpMethod]: {
summary: description,
operationId: `${service.name}_${method.name}`,
}
};
const inputMessage = registry.resolveTypeName(method.inputType);
const outPutMessage = registry.resolveTypeName(method.outputType);
// All methods except GET have body
if (httpMethod !== gateway_1.Pattern.GET) {
pathItem[httpMethod].requestBody = genGatewayBody(ctx, httpSpec, inputMessage);
}
// All methods might have params
pathItem[httpMethod].parameters = genGatewayParams(ctx, httpSpec, inputMessage);
pathItem[httpMethod].responses = genGatewayResponse(ctx, httpSpec, outPutMessage);
paths[`${httpSpec[httpMethod]}`] = pathItem;
if (httpSpec.additional_bindings) {
buildPath(method, httpSpec.additional_bindings, paths);
}
}
return service.method.reduce((paths, method) => {
const options = ctx.interpreter.readOptions(method);
if (!options || options && !options["google.api.http"]) {
return paths;
}
const httpSpec = options["google.api.http"];
buildPath(method, httpSpec, paths);
return paths;
}, {});
}
/**
* Generate OpenAPI Gateway Response
* @param ctx
* @param httpOptions
* @param message
*/
function genGatewayResponse(ctx, httpOptions, message) {
let schema = {};
if (httpOptions.responseBody) {
schema = {
type: "object",
properties: {
[httpOptions.responseBody]: {
$ref: `#/components/schemas/${message.name}`
}
}
};
}
else {
schema = {
$ref: `#/components/schemas/${message.name}`
};
}
return {
"200": {
description: "OK",
content: {
"application/json": {
schema,
}
}
}
};
}
/**
* Generate OpenAPI Gateway Response
* @param ctx
* @param httpOptions
* @param message
*/
function genGatewayBody(ctx, httpOptions, message) {
const schema = {};
if (httpOptions.body === "*") {
schema.$ref = `#/components/schemas/${message.name}`;
}
else if (httpOptions.body) {
const subField = message.field.find(field => field.name === httpOptions.body);
if (!subField) {
throw new Error(`the body field ${httpOptions.body} cannot be mapped to message ${message.name}`);
}
schema.properties = {
[httpOptions.body]: genField(ctx, subField),
};
}
return {
required: true,
content: {
"application/json": {
schema,
}
}
};
}
/**
* Generates OpenAPI Gateway Parameters
* @param ctx
* @param httpOptions
* @param message
*/
function genGatewayParams(ctx, httpOptions, message) {
const httpMethod = gateway_1.getMethod(httpOptions);
const params = parseUriParams(httpOptions[httpMethod]);
const urlParams = message.field
.filter((field) => params.find((param) => param === field.name))
.map((field) => {
return {
name: field.name,
in: "path",
required: true,
schema: Object.assign({}, genField(ctx, field))
};
});
if (httpOptions.body === "*") {
return urlParams;
}
const queryString = message.field
.filter((field) => field.name !== httpOptions.body &&
!params.find(param => param === field.name))
.map((field) => {
return {
name: field.name,
in: "query",
schema: Object.assign({}, genField(ctx, field))
};
});
return [
...queryString,
...urlParams,
];
}
/**
* Generates OpenAPI Components
* @param ctx
* @param methods
*/
function genComponents(ctx, methods) {
const components = {
schemas: {}
};
methods.reduce((schemas, method) => {
genSchema(ctx, schemas, method.inputType);
genSchema(ctx, schemas, method.outputType);
return schemas;
}, components.schemas);
return components;
}
/**
* Generate OpenAPI Schemas
* @param ctx
* @param schemas
* @param typeName
*/
function genSchema(ctx, schemas, typeName) {
const registry = ctx.registry;
const localName = localMessageName(ctx, typeName);
if (!localName) {
return;
}
const descriptor = registry.resolveTypeName(typeName);
if (schemas[localName]) {
return;
}
// Handle OneOf
if (descriptor.field.some((field) => registry.isUserDeclaredOneof(field))) {
schemas[localName] = genOneOfType(ctx, descriptor);
descriptor.oneofDecl.forEach((oneOfField, index) => {
const oneOfTyName = `${localName}_${capitalizeFirstLetter(oneOfField.name)}`;
const oneOfFields = descriptor.field.filter(field => {
return field.oneofIndex === index;
});
schemas[oneOfTyName] = genOneOfTypeKind(ctx, descriptor, oneOfFields);
});
}
else {
schemas[localName] = genType(ctx, descriptor);
}
descriptor.field.forEach((field) => {
if (field.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE || !registry.isMapField(field)) {
return;
}
if (registry.isMapField(field)) {
const entry = registry.resolveTypeName(field.typeName);
if (plugin_framework_1.DescriptorProto.is(entry)) {
const valueField = entry.field.find(fd => fd.number === 2);
if (!valueField) {
return;
}
if (valueField.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE) {
return;
}
field = valueField;
}
}
else if (registry.isSyntheticElement(descriptor)) {
return;
}
genSchema(ctx, schemas, field.typeName);
});
}
/**
* Generate an OpenAPI type
* @param ctx
* @param message
*/
function genType(ctx, message) {
const description = genDescription(ctx, message);
return {
properties: genMessageProperties(ctx, message),
description,
};
}
/**
* Generate a Protobuf to OpenAPI oneof type
* @param ctx
* @param message
*/
function genOneOfType(ctx, message) {
const description = genDescription(ctx, message);
const oneOf = {
allOf: [
{
type: "object",
properties: genMessageProperties(ctx, message),
},
],
description,
};
message.oneofDecl.forEach((field) => {
oneOf.allOf.push({
$ref: `#/components/schemas/${message.name}_${capitalizeFirstLetter(field.name)}`
});
});
return oneOf;
}
/**
* Generate one of type
* @param ctx
* @param message
* @param oneOfFields
*/
function genOneOfTypeKind(ctx, message, oneOfFields) {
return {
oneOf: oneOfFields.map((oneOf) => {
return {
type: "object",
properties: {
[oneOf.name]: genField(ctx, oneOf),
}
};
})
};
}
/**
* Generate message properties
* @param ctx
* @param message
*/
function genMessageProperties(ctx, message) {
const registry = ctx.registry;
return message.field.reduce((fields, field) => {
if (registry.isUserDeclaredOneof(field)) {
return fields;
}
fields[field.name] = genField(ctx, field);
return fields;
}, {});
}
/**
* Generates OpenAPI $ref
* @param ctx
* @param name
*/
function genRef(ctx, name) {
const messageType = localMessageName(ctx, name);
return `#/components/schemas/${messageType}`;
}
/**
* Generate field definition
* @param ctx
* @param field
*/
function genField(ctx, field) {
let openApiType;
const registry = ctx.registry;
switch (field.type) {
case plugin_framework_1.FieldDescriptorProto_Type.DOUBLE:
case plugin_framework_1.FieldDescriptorProto_Type.FLOAT:
case plugin_framework_1.FieldDescriptorProto_Type.BOOL:
case plugin_framework_1.FieldDescriptorProto_Type.STRING:
case plugin_framework_1.FieldDescriptorProto_Type.FIXED32:
case plugin_framework_1.FieldDescriptorProto_Type.FIXED64:
case plugin_framework_1.FieldDescriptorProto_Type.INT32:
case plugin_framework_1.FieldDescriptorProto_Type.INT64:
case plugin_framework_1.FieldDescriptorProto_Type.SFIXED32:
case plugin_framework_1.FieldDescriptorProto_Type.SFIXED64:
case plugin_framework_1.FieldDescriptorProto_Type.SINT32:
case plugin_framework_1.FieldDescriptorProto_Type.SINT64:
case plugin_framework_1.FieldDescriptorProto_Type.UINT32:
case plugin_framework_1.FieldDescriptorProto_Type.UINT64:
openApiType = {
type: genScalar(field.type),
};
break;
case plugin_framework_1.FieldDescriptorProto_Type.BYTES:
openApiType = {
type: "array",
items: {
type: "integer",
}
};
break;
case plugin_framework_1.FieldDescriptorProto_Type.ENUM:
const enumType = registry.getEnumFieldEnum(field);
openApiType = genEnum(enumType);
break;
case plugin_framework_1.FieldDescriptorProto_Type.MESSAGE:
// Map type
if (registry.isMapField(field)) {
const mapTypeValue = registry.getMapValueType(field);
if (typeof mapTypeValue === "number") {
const scalar = mapTypeValue;
openApiType = {
type: "object",
additionalProperties: {
type: genScalar(scalar)
}
};
}
else if (plugin_framework_1.EnumDescriptorProto.is(mapTypeValue)) {
openApiType = {
type: "object",
additionalProperties: Object.assign({}, genEnum(mapTypeValue))
};
}
else if (plugin_framework_1.DescriptorProto.is(mapTypeValue)) {
openApiType = {
type: "object",
additionalProperties: {
$ref: `#/components/schemas/${mapTypeValue.name}`,
}
};
}
else {
throw new Error("map value not supported");
}
break;
}
openApiType = {
$ref: genRef(ctx, field.typeName),
};
break;
default:
throw new Error(`${field.name} of type ${field.type} not supported`);
}
const description = genDescription(ctx, field);
if (field.label === plugin_framework_1.FieldDescriptorProto_Label.REPEATED && !registry.isMapField(field)) {
return {
type: "array",
items: openApiType,
description: description || "",
};
}
if (field.type !== plugin_framework_1.FieldDescriptorProto_Type.MESSAGE) {
openApiType.description = description || "";
}
return openApiType;
}
/**
* Generates enum definition
* @param enumType
*/
function genEnum(enumType) {
return {
type: 'string',
enum: enumType.value.map((value) => {
return value.name;
})
};
}
/**
* Generate scalar
* @param type
*/
function genScalar(type) {
switch (type) {
case plugin_framework_1.FieldDescriptorProto_Type.BOOL:
return "boolean";
case plugin_framework_1.FieldDescriptorProto_Type.DOUBLE:
case plugin_framework_1.FieldDescriptorProto_Type.FLOAT:
return "number";
case plugin_framework_1.FieldDescriptorProto_Type.STRING:
return "string";
case plugin_framework_1.FieldDescriptorProto_Type.FIXED32:
case plugin_framework_1.FieldDescriptorProto_Type.FIXED64:
case plugin_framework_1.FieldDescriptorProto_Type.INT32:
case plugin_framework_1.FieldDescriptorProto_Type.INT64:
case plugin_framework_1.FieldDescriptorProto_Type.SFIXED32:
case plugin_framework_1.FieldDescriptorProto_Type.SFIXED64:
case plugin_framework_1.FieldDescriptorProto_Type.SINT32:
case plugin_framework_1.FieldDescriptorProto_Type.SINT64:
case plugin_framework_1.FieldDescriptorProto_Type.UINT32:
case plugin_framework_1.FieldDescriptorProto_Type.UINT64:
return "integer";
default:
throw new Error(`${type} is not a scalar value`);
}
}
/**
* Generates the description
* @param ctx
* @param descriptor
*/
function genDescription(ctx, descriptor) {
const registry = ctx.registry;
const source = registry.sourceCodeComments(descriptor);
const description = source.leading || source.trailing || "";
return description.trim();
}
/**
* Format protobuf name
* @param ctx
* @param name
*/
function localMessageName(ctx, name) {
const registry = ctx.registry;
const symbols = ctx.symbols;
const entry = symbols.find(registry.resolveTypeName(name));
if (!entry) {
return "";
}
return local_type_name_1.createLocalTypeName(entry.descriptor, registry);
}
function parseUriParams(uri) {
return getMatches(uri, /{([a-zA-Z_0-9]+)}/g, 1);
}
function getMatches(str, regex, index = 1) {
const matches = [];
let match;
while (match = regex.exec(str)) {
matches.push(match[index]);
}
return matches;
}
function capitalizeFirstLetter(str) {
return str.charAt(0).toUpperCase() + str.slice(1);
}

View file

@ -0,0 +1,20 @@
import { FileDescriptorProto } from "@protobuf-ts/plugin-framework";
/**
* Generates the client and server implementation of the twirp
* specification.
* @param ctx
* @param file
*/
export declare function generateTwirp(ctx: any, file: FileDescriptorProto): Promise<string>;
/**
* Generates the client implementation of the twirp specification.
* @param ctx
* @param file
*/
export declare function generateTwirpClient(ctx: any, file: FileDescriptorProto): Promise<string>;
/**
* Generates the server implementation of the twirp specification.
* @param ctx
* @param file
*/
export declare function generateTwirpServer(ctx: any, file: FileDescriptorProto): Promise<string>;

View file

@ -0,0 +1,448 @@
"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;
}

View file

@ -0,0 +1,97 @@
import { DescriptorProto, DescriptorRegistry, EnumDescriptorProto, FieldDescriptorProto, FieldOptions_JSType, FileDescriptorProto, MethodDescriptorProto, ServiceDescriptorProto } from "@protobuf-ts/plugin-framework";
import * as rt from "@protobuf-ts/runtime";
declare type JsonOptionsMap = {
[extensionName: string]: rt.JsonValue;
};
/**
* Code borrowed from @protobuf-js/plugin all the rights of this code goes to the author
*
*
* The protobuf-ts plugin generates code for message types from descriptor
* protos. This class also creates message types from descriptor protos, but
* but instead of generating code, it creates the type in-memory.
*
* This means that it is possible, for example, to read a message from binary
* data without any generated code.
*
* The protobuf-ts plugin uses the interpreter to read custom options at
* compile time and convert them to JSON.
*
* Since the interpreter creates fully functional message types including
* reflection information, the protobuf-ts plugin uses the interpreter as
* single source of truth for generating message interfaces and reflection
* information.
*/
export declare class Interpreter {
private readonly registry;
private readonly messageTypes;
private readonly enumInfos;
constructor(registry: DescriptorRegistry);
/**
* Returns a map of custom options for the provided descriptor.
* The map is an object indexed by the extension field name.
* The value of the extension field is provided in JSON format.
*
* This works by:
* - searching for option extensions for the given descriptor proto
* in the registry.
* - for example, providing a google.protobuf.FieldDescriptorProto
* searches for all extensions on google.protobuf.FieldOption.
* - extensions are just fields, so we build a synthetic message
* type with all the (extension) fields.
* - the field names are created by DescriptorRegistry.getExtensionName(),
* which produces for example "spec.option_name", where "spec" is
* the package and "option_name" is the field name.
* - then we concatenate all unknown field data of the option and
* read the data with our synthetic message type
* - the read message is then simply converted to JSON
*
* The optional "optionBlacklist" will exclude matching options.
* The blacklist can contain exact extension names, or use the wildcard
* character `*` to match a namespace or even all options.
*
* Note that options on options (google.protobuf.*Options) are not
* supported.
*/
readOptions(descriptor: FieldDescriptorProto | MethodDescriptorProto | FileDescriptorProto | ServiceDescriptorProto | DescriptorProto, excludeOptions?: readonly string[]): JsonOptionsMap | undefined;
/**
* Get a runtime type for the given message type name or message descriptor.
* Creates the type if not created previously.
*
* Honors our file option "ts.exclude_options".
*/
getMessageType(descriptorOrTypeName: string | DescriptorProto): rt.IMessageType<rt.UnknownMessage>;
/**
* Get runtime information for an enum.
* Creates the info if not created previously.
*/
getEnumInfo(descriptorOrTypeName: string | EnumDescriptorProto): rt.EnumInfo;
/**
* Create a name for a field or a oneof.
* - use lowerCamelCase
* - escape reserved object property names by
* adding '$' at the end
* - don't have to escape reserved keywords
*/
private static createTypescriptNameForField;
private buildMessageType;
private buildFieldInfos;
private buildFieldInfo;
protected buildEnumInfo(descriptor: EnumDescriptorProto): rt.EnumInfo;
protected determineNonDefaultLongType(scalarType: rt.ScalarType, jsTypeOption?: FieldOptions_JSType): rt.LongType | undefined;
/**
* Is this a 64 bit integral or fixed type?
*/
static isLongValueType(type: rt.ScalarType): boolean;
}
/**
* Builds a typescript enum lookup object,
* compatible with enums generated by @protobuf-ts/plugin.
*/
export declare class RuntimeEnumBuilder {
private readonly values;
add(name: string, number: number): void;
isValid(): boolean;
build(): rt.EnumInfo[1];
}
export {};

View file

@ -0,0 +1,436 @@
"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
Object.defineProperty(o, k2, { enumerable: true, get: function() { return m[k]; } });
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
__setModuleDefault(result, mod);
return result;
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.RuntimeEnumBuilder = exports.Interpreter = void 0;
const plugin_framework_1 = require("@protobuf-ts/plugin-framework");
const rt = __importStar(require("@protobuf-ts/runtime"));
const runtime_1 = require("@protobuf-ts/runtime");
/**
* Code borrowed from @protobuf-js/plugin all the rights of this code goes to the author
*
*
* The protobuf-ts plugin generates code for message types from descriptor
* protos. This class also creates message types from descriptor protos, but
* but instead of generating code, it creates the type in-memory.
*
* This means that it is possible, for example, to read a message from binary
* data without any generated code.
*
* The protobuf-ts plugin uses the interpreter to read custom options at
* compile time and convert them to JSON.
*
* Since the interpreter creates fully functional message types including
* reflection information, the protobuf-ts plugin uses the interpreter as
* single source of truth for generating message interfaces and reflection
* information.
*/
class Interpreter {
constructor(registry) {
this.registry = registry;
this.messageTypes = new Map();
this.enumInfos = new Map();
}
/**
* Returns a map of custom options for the provided descriptor.
* The map is an object indexed by the extension field name.
* The value of the extension field is provided in JSON format.
*
* This works by:
* - searching for option extensions for the given descriptor proto
* in the registry.
* - for example, providing a google.protobuf.FieldDescriptorProto
* searches for all extensions on google.protobuf.FieldOption.
* - extensions are just fields, so we build a synthetic message
* type with all the (extension) fields.
* - the field names are created by DescriptorRegistry.getExtensionName(),
* which produces for example "spec.option_name", where "spec" is
* the package and "option_name" is the field name.
* - then we concatenate all unknown field data of the option and
* read the data with our synthetic message type
* - the read message is then simply converted to JSON
*
* The optional "optionBlacklist" will exclude matching options.
* The blacklist can contain exact extension names, or use the wildcard
* character `*` to match a namespace or even all options.
*
* Note that options on options (google.protobuf.*Options) are not
* supported.
*/
readOptions(descriptor, excludeOptions = []) {
// if options message not present, there cannot be any extension options
if (!descriptor.options) {
return undefined;
}
// if no unknown fields present, can exit early
let unknownFields = rt.UnknownFieldHandler.list(descriptor.options);
if (!unknownFields.length) {
return undefined;
}
let optionsTypeName;
if (plugin_framework_1.FieldDescriptorProto.is(descriptor) &&
plugin_framework_1.DescriptorProto.is(this.registry.parentOf(descriptor))) {
optionsTypeName = "google.protobuf.FieldOptions";
}
else if (plugin_framework_1.MethodDescriptorProto.is(descriptor)) {
optionsTypeName = "google.protobuf.MethodOptions";
}
else if (this.registry.fileOf(descriptor) === descriptor) {
optionsTypeName = "google.protobuf.FileOptions";
}
else if (plugin_framework_1.ServiceDescriptorProto.is(descriptor)) {
optionsTypeName = "google.protobuf.ServiceOptions";
}
else if (plugin_framework_1.DescriptorProto.is(descriptor)) {
optionsTypeName = "google.protobuf.MessageOptions";
}
else {
throw new Error("interpreter expected field or method descriptor");
}
// create a synthetic type that has all extension fields for field options
const typeName = `$synthetic.${optionsTypeName}`;
let type = this.messageTypes.get(typeName);
if (!type) {
type = new rt.MessageType(typeName, this.buildFieldInfos(this.registry.extensionsFor(optionsTypeName), excludeOptions), {});
this.messageTypes.set(typeName, type);
}
// concat all unknown field data
const unknownWriter = new rt.BinaryWriter();
for (let { no, wireType, data } of unknownFields) {
unknownWriter.tag(no, wireType).raw(data);
}
const unknownBytes = unknownWriter.finish();
// read data, to json
const json = type.toJson(type.fromBinary(unknownBytes, { readUnknownField: false }));
runtime_1.assert(rt.isJsonObject(json));
// apply blacklist
if (excludeOptions) {
// we distinguish between literal blacklist (no wildcard)
let literals = excludeOptions.filter((str) => !str.includes("*"));
// and wildcard, which we turn into RE
let wildcards = excludeOptions
.filter((str) => str.includes("*"))
.map((str) => str.replace(/[.+\-?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"));
// then we delete the blacklisted options
for (let key of Object.keys(json)) {
for (let str of literals)
if (key === str)
delete json[key];
for (let re of wildcards)
if (key.match(re))
delete json[key];
}
}
// were *all* options blacklisted?
if (!Object.keys(json).length) {
return undefined;
}
return json;
}
/**
* Get a runtime type for the given message type name or message descriptor.
* Creates the type if not created previously.
*
* Honors our file option "ts.exclude_options".
*/
getMessageType(descriptorOrTypeName) {
let descriptor = typeof descriptorOrTypeName === "string"
? this.registry.resolveTypeName(descriptorOrTypeName)
: descriptorOrTypeName;
let typeName = this.registry.makeTypeName(descriptor);
runtime_1.assert(plugin_framework_1.DescriptorProto.is(descriptor));
let type = this.messageTypes.get(typeName);
if (!type) {
type = this.buildMessageType(typeName, descriptor.field, []);
this.messageTypes.set(typeName, type);
}
return type;
}
/**
* Get runtime information for an enum.
* Creates the info if not created previously.
*/
getEnumInfo(descriptorOrTypeName) {
var _a;
let descriptor = typeof descriptorOrTypeName === "string"
? this.registry.resolveTypeName(descriptorOrTypeName)
: descriptorOrTypeName;
let typeName = this.registry.makeTypeName(descriptor);
runtime_1.assert(plugin_framework_1.EnumDescriptorProto.is(descriptor));
let enumInfo = (_a = this.enumInfos.get(typeName)) !== null && _a !== void 0 ? _a : this.buildEnumInfo(descriptor);
this.enumInfos.set(typeName, enumInfo);
return enumInfo;
}
/**
* Create a name for a field or a oneof.
* - use lowerCamelCase
* - escape reserved object property names by
* adding '$' at the end
* - don't have to escape reserved keywords
*/
static createTypescriptNameForField(descriptor, additionalReservedWords = "", escapeCharacter = "$") {
const reservedObjectProperties = "__proto__,toString".split(",");
let name = descriptor.name;
runtime_1.assert(name !== undefined);
name = rt.lowerCamelCase(name);
if (reservedObjectProperties.includes(name)) {
name = name + escapeCharacter;
}
if (additionalReservedWords.split(",").includes(name)) {
name = name + escapeCharacter;
}
return name;
}
buildMessageType(typeName, fields, excludeOptions) {
let desc = this.registry.resolveTypeName(typeName);
runtime_1.assert(plugin_framework_1.DescriptorProto.is(desc));
return new rt.MessageType(typeName, this.buildFieldInfos(fields, excludeOptions), this.readOptions(desc, excludeOptions));
}
// skips GROUP field type
buildFieldInfos(fieldDescriptors, excludeOptions) {
const result = [];
for (const fd of fieldDescriptors) {
if (this.registry.isGroupField(fd)) {
// We ignore groups.
// Note that groups are deprecated and not supported in proto3.
continue;
}
const fi = this.buildFieldInfo(fd, excludeOptions);
if (fi) {
result.push(fi);
}
}
return result;
}
// throws on unexpected field types, notably GROUP
buildFieldInfo(fieldDescriptor, excludeOptions) {
var _a, _b;
runtime_1.assert(fieldDescriptor.number);
runtime_1.assert(fieldDescriptor.name);
let info = {};
// no: The field number of the .proto field.
info.no = fieldDescriptor.number;
// name: The original name of the .proto field.
info.name = fieldDescriptor.name;
// kind: discriminator
info.kind = undefined;
// localName: The name of the field in the runtime.
let localName = Interpreter.createTypescriptNameForField(fieldDescriptor);
if (localName !== rt.lowerCamelCase(fieldDescriptor.name)) {
info.localName = localName;
}
// jsonName: The name of the field in JSON.
const jsonName = this.registry.getFieldCustomJsonName(fieldDescriptor);
if (jsonName !== undefined) {
info.jsonName = jsonName;
}
// oneof: The name of the `oneof` group, if this field belongs to one.
if (this.registry.isUserDeclaredOneof(fieldDescriptor)) {
runtime_1.assert(fieldDescriptor.oneofIndex !== undefined);
const parentDescriptor = this.registry.parentOf(fieldDescriptor);
runtime_1.assert(plugin_framework_1.DescriptorProto.is(parentDescriptor));
const ooDecl = parentDescriptor.oneofDecl[fieldDescriptor.oneofIndex];
info.oneof = Interpreter.createTypescriptNameForField(ooDecl);
}
// repeat: Is the field repeated?
if (this.registry.isUserDeclaredRepeated(fieldDescriptor)) {
let packed = this.registry.shouldBePackedRepeated(fieldDescriptor);
info.repeat = packed ? rt.RepeatType.PACKED : rt.RepeatType.UNPACKED;
}
// opt: Is the field optional?
if (this.registry.isScalarField(fieldDescriptor) ||
this.registry.isEnumField(fieldDescriptor)) {
if (this.registry.isUserDeclaredOptional(fieldDescriptor)) {
info.opt = true;
}
}
// jsonName: The name for JSON serialization / deserialization.
if (fieldDescriptor.jsonName) {
info.jsonName = fieldDescriptor.jsonName;
}
if (this.registry.isScalarField(fieldDescriptor)) {
// kind:
info.kind = "scalar";
// T: Scalar field type.
info.T = this.registry.getScalarFieldType(fieldDescriptor);
// L?: JavaScript long type
let L = this.determineNonDefaultLongType(info.T, (_a = fieldDescriptor.options) === null || _a === void 0 ? void 0 : _a.jstype);
if (L !== undefined) {
info.L = L;
}
}
else if (this.registry.isEnumField(fieldDescriptor)) {
// kind:
info.kind = "enum";
// T: Return enum field type info.
info.T = () => this.getEnumInfo(this.registry.getEnumFieldEnum(fieldDescriptor));
}
else if (this.registry.isMessageField(fieldDescriptor)) {
// kind:
info.kind = "message";
// T: Return message field type handler.
info.T = () => this.getMessageType(this.registry.getMessageFieldMessage(fieldDescriptor));
}
else if (this.registry.isMapField(fieldDescriptor)) {
// kind:
info.kind = "map";
// K: Map field key type.
info.K = this.registry.getMapKeyType(fieldDescriptor);
// V: Map field value type.
info.V = {};
let mapV = this.registry.getMapValueType(fieldDescriptor);
if (typeof mapV === "number") {
info.V = {
kind: "scalar",
T: mapV,
};
let L = this.determineNonDefaultLongType(info.V.T, (_b = fieldDescriptor.options) === null || _b === void 0 ? void 0 : _b.jstype);
if (L !== undefined) {
info.V.L = L;
}
}
else if (plugin_framework_1.DescriptorProto.is(mapV)) {
const messageDescriptor = mapV;
info.V = {
kind: "message",
T: () => this.getMessageType(messageDescriptor),
};
}
else {
const enumDescriptor = mapV;
info.V = {
kind: "enum",
T: () => this.getEnumInfo(enumDescriptor),
};
}
}
else {
throw new Error(`Unexpected field type for ${this.registry.formatQualifiedName(fieldDescriptor)}`);
}
// extension fields are treated differently
if (this.registry.isExtension(fieldDescriptor)) {
let extensionName = this.registry.getExtensionName(fieldDescriptor);
// always optional (unless repeated...)
info.opt = info.repeat === undefined || info.repeat === rt.RepeatType.NO;
info.name = extensionName;
info.localName = extensionName;
info.jsonName = extensionName;
info.oneof = undefined;
}
else {
info.options = this.readOptions(fieldDescriptor, excludeOptions);
}
return info;
}
buildEnumInfo(descriptor) {
let sharedPrefix = this.registry.findEnumSharedPrefix(descriptor, `${descriptor.name}`);
let hasZero = descriptor.value.some((v) => v.number === 0);
let builder = new RuntimeEnumBuilder();
if (!hasZero) {
throw new Error("must provide zero value for enum " + descriptor.name);
}
for (let enumValueDescriptor of descriptor.value) {
let name = enumValueDescriptor.name;
runtime_1.assert(name !== undefined);
runtime_1.assert(enumValueDescriptor.number !== undefined);
if (sharedPrefix) {
name = name.substring(sharedPrefix.length);
}
builder.add(name, enumValueDescriptor.number);
}
let enumInfo = [
this.registry.makeTypeName(descriptor),
builder.build(),
];
if (sharedPrefix) {
enumInfo = [enumInfo[0], enumInfo[1], sharedPrefix];
}
return enumInfo;
}
determineNonDefaultLongType(scalarType, jsTypeOption) {
if (!Interpreter.isLongValueType(scalarType)) {
return undefined;
}
if (jsTypeOption !== undefined) {
switch (jsTypeOption) {
case plugin_framework_1.FieldOptions_JSType.JS_STRING:
// omitting L equals to STRING
return undefined;
case plugin_framework_1.FieldOptions_JSType.JS_NORMAL:
return rt.LongType.BIGINT;
case plugin_framework_1.FieldOptions_JSType.JS_NUMBER:
return rt.LongType.NUMBER;
}
}
return undefined;
}
/**
* Is this a 64 bit integral or fixed type?
*/
static isLongValueType(type) {
switch (type) {
case rt.ScalarType.INT64:
case rt.ScalarType.UINT64:
case rt.ScalarType.FIXED64:
case rt.ScalarType.SFIXED64:
case rt.ScalarType.SINT64:
return true;
default:
return false;
}
}
}
exports.Interpreter = Interpreter;
/**
* Builds a typescript enum lookup object,
* compatible with enums generated by @protobuf-ts/plugin.
*/
class RuntimeEnumBuilder {
constructor() {
this.values = [];
}
add(name, number) {
this.values.push({ name, number });
}
isValid() {
try {
this.build();
}
catch (e) {
return false;
}
return true;
}
build() {
if (this.values.map((v) => v.name).some((name, i, a) => a.indexOf(name) !== i)) {
throw new Error("duplicate names");
}
let object = {};
for (let v of this.values) {
object[v.number] = v.name;
object[v.name] = v.number;
}
if (rt.isEnumObject(object)) {
return object;
}
throw new Error("not a typescript enum object");
}
}
exports.RuntimeEnumBuilder = RuntimeEnumBuilder;

View file

@ -0,0 +1,12 @@
import { AnyTypeDescriptorProto, IDescriptorTree } from "@protobuf-ts/plugin-framework";
/**
* Code borrowed from @protobuf-js/plugin all the rights of this code goes to the author
*
* Create a name for an enum, message or service.
* - ignores package
* - nested types get the names merged with '_'
* - reserved words are escaped by adding '$' at the end
* - does *not* prevent clashes, for example clash
* of merged nested name with other message name
*/
export declare function createLocalTypeName(descriptor: AnyTypeDescriptorProto, treeLookup: IDescriptorTree): string;

View file

@ -0,0 +1,45 @@
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.createLocalTypeName = void 0;
const plugin_framework_1 = require("@protobuf-ts/plugin-framework");
const runtime_1 = require("@protobuf-ts/runtime");
const reservedKeywords = "break,case,catch,class,const,continue,debugger,default,delete,do,else,enum,export,extends,false,finally,for,function,if,import,in,instanceof,new,null,return,super,switch,this,throw,true,try,typeof,var,void,while,with,as,implements,interface,let,package,private,protected,public,static,yield,any,boolean,constructor,declare,get,module,require,number,set,string,symbol,type,from,of".split(",");
const reservedTypeNames = "Long,Uint8Array,array,Array,string,String,number,Number,boolean,Boolean,bigint,BigInt".split(",");
const escapeCharacter = "$";
/**
* Code borrowed from @protobuf-js/plugin all the rights of this code goes to the author
*
* Create a name for an enum, message or service.
* - ignores package
* - nested types get the names merged with '_'
* - reserved words are escaped by adding '$' at the end
* - does *not* prevent clashes, for example clash
* of merged nested name with other message name
*/
function createLocalTypeName(descriptor, treeLookup) {
// build name components for parent types
const components = [];
for (const ancestor of treeLookup.ancestorsOf(descriptor)) {
if (plugin_framework_1.FileDescriptorProto.is(ancestor)) {
continue;
}
const name = ancestor.name;
runtime_1.assert(name !== undefined);
components.push(name);
}
// add name for actual descriptor
const name = descriptor.name;
runtime_1.assert(name !== undefined);
components.push(name);
// join all components with underscore
let fullName = components.join("_");
// escape if reserved
if (reservedKeywords.includes(fullName)) {
fullName = fullName + escapeCharacter;
}
if (reservedTypeNames.includes(fullName)) {
fullName = fullName + escapeCharacter;
}
return fullName;
}
exports.createLocalTypeName = createLocalTypeName;

View file

@ -0,0 +1,38 @@
import { CodeGeneratorRequest, CodeGeneratorResponse_Feature, PluginBase } from "@protobuf-ts/plugin-framework";
import { File } from "./file";
export declare class ProtobuftsPlugin extends PluginBase<File> {
parameters: {
ts_proto: {
description: string;
};
gateway: {
description: string;
};
index_file: {
description: string;
};
emit_default_values: {
description: string;
};
openapi_twirp: {
description: string;
};
openapi_gateway: {
description: string;
};
standalone: {
description: string;
};
client_only: {
description: string;
};
server_only: {
description: string;
};
camel_case: {
description: string;
};
};
generate(request: CodeGeneratorRequest): Promise<File[]>;
protected getSupportedFeatures: () => CodeGeneratorResponse_Feature[];
}

View file

@ -0,0 +1,145 @@
"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());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ProtobuftsPlugin = void 0;
const plugin_framework_1 = require("@protobuf-ts/plugin-framework");
const file_1 = require("./file");
const twirp_1 = require("./gen/twirp");
const gateway_1 = require("./gen/gateway");
const local_type_name_1 = require("./local-type-name");
const interpreter_1 = require("./interpreter");
const open_api_1 = require("./gen/open-api");
const index_file_1 = require("./gen/index-file");
class ProtobuftsPlugin extends plugin_framework_1.PluginBase {
constructor() {
super(...arguments);
this.parameters = {
ts_proto: {
description: "Use the ts-proto compiler (protobuf-ts by default)",
},
gateway: {
description: "Generates the twirp gateway",
},
index_file: {
description: "Generates an index.ts file that exports all the types",
},
emit_default_values: {
description: "Json encode and decode will emit default values",
},
openapi_twirp: {
description: "Generates an OpenAPI spec for twirp handlers",
},
openapi_gateway: {
description: "Generates an OpenAPI spec for gateway handlers",
},
standalone: {
description: "Generates client and server in 2 separate files",
},
client_only: {
description: "Only client will be generated (overrides 'standalone')",
},
server_only: {
description: "Only server will be generated (overrides 'standalone')",
},
camel_case: {
description: "Generates with method names in camel case.",
},
};
// we support proto3-optionals, so we let protoc know
this.getSupportedFeatures = () => [
plugin_framework_1.CodeGeneratorResponse_Feature.PROTO3_OPTIONAL,
];
}
generate(request) {
var _a, _b, _c, _d;
return __awaiter(this, void 0, void 0, function* () {
const params = this.parseOptions(this.parameters, request.parameter), registry = plugin_framework_1.DescriptorRegistry.createFrom(request), symbols = new plugin_framework_1.SymbolTable(), interpreter = new interpreter_1.Interpreter(registry);
const ctx = {
lib: params.ts_proto ? "ts-proto" : "protobuf-ts",
emitDefaultValues: params.emit_default_values,
symbols,
registry,
interpreter,
camelCase: params.camel_case,
};
const files = [];
for (let fileDescriptor of registry.allFiles()) {
const messageFileOut = new file_1.File(`${(_a = fileDescriptor.name) === null || _a === void 0 ? void 0 : _a.replace(".proto", "").toLowerCase()}`);
registry.visitTypes(fileDescriptor, (descriptor) => {
// we are not interested in synthetic types like map entry messages
if (registry.isSyntheticElement(descriptor))
return;
ctx.symbols.register(local_type_name_1.createLocalTypeName(descriptor, registry), descriptor, messageFileOut);
});
// Generate a combined client and server bundle if no code gen
// options are passed.
if (!params.standalone && !params.client_only && !params.server_only) {
const twirpFileOut = new file_1.File(`${(_b = fileDescriptor.name) === null || _b === void 0 ? void 0 : _b.replace(".proto", "").toLowerCase()}.twirp.ts`);
const twirpFileContent = yield twirp_1.generateTwirp(ctx, fileDescriptor);
twirpFileOut.setContent(twirpFileContent);
files.push(twirpFileOut);
}
if (params.server_only && params.client_only) {
throw new Error("Only one of server_only or client_only can be passed.");
}
if (params.server_only || params.standalone) {
const serverFileOut = new file_1.File(`${(_c = fileDescriptor.name) === null || _c === void 0 ? void 0 : _c.replace(".proto", "").toLowerCase()}.twirp.ts`);
const serverContent = yield twirp_1.generateTwirpServer(ctx, fileDescriptor);
serverFileOut.setContent(serverContent);
files.push(serverFileOut);
}
if (params.client_only || params.standalone) {
const clientFileOut = new file_1.File(`${(_d = fileDescriptor.name) === null || _d === void 0 ? void 0 : _d.replace(".proto", "").toLowerCase()}.twirp-client.ts`);
const clientContent = yield twirp_1.generateTwirpClient(ctx, fileDescriptor);
clientFileOut.setContent(clientContent);
files.push(clientFileOut);
}
}
// Gateway generation
if (params.gateway) {
const gatewayFileOut = new file_1.File(`gateway.twirp.ts`);
const gatewayContent = yield gateway_1.genGateway(ctx, registry.allFiles());
gatewayFileOut.setContent(gatewayContent);
files.push(gatewayFileOut);
}
// Create index file
if (params.index_file) {
files.push(index_file_1.genIndexFile(registry, [...files]));
}
// Open API
const docs = [];
if (params.openapi_twirp) {
docs.push(...(yield open_api_1.genOpenAPI(ctx, registry.allFiles(), open_api_1.OpenAPIType.TWIRP)));
}
if (params.openapi_gateway) {
docs.push(...(yield open_api_1.genOpenAPI(ctx, registry.allFiles(), open_api_1.OpenAPIType.GATEWAY)));
}
docs.forEach((doc) => {
const file = new file_1.File(`${doc.fileName}`);
file.setContent(doc.content);
files.push(file);
});
return files;
});
}
}
exports.ProtobuftsPlugin = ProtobuftsPlugin;
new ProtobuftsPlugin()
.run()
.then(() => {
process.exit(0);
})
.catch((e) => {
process.stderr.write("FAILED!");
process.stderr.write(e.message);
process.stderr.write(e.stack);
process.exit(1);
});