Skip to content

Commit

Permalink
Merge pull request #2183 from install/template
Browse files Browse the repository at this point in the history
proto-loader-gen-types Typename templates
  • Loading branch information
murgatroid99 committed Sep 21, 2022
2 parents c023d05 + d81ec8e commit 04b14eb
Show file tree
Hide file tree
Showing 51 changed files with 452 additions and 422 deletions.
4 changes: 4 additions & 0 deletions packages/proto-loader/README.md
Expand Up @@ -88,6 +88,10 @@ Options:
-O, --outDir Directory in which to output files [string] [required]
--grpcLib The gRPC implementation library that these types will
be used with [string] [required]
--inputTemplate Template for mapping input or "permissive" type names
[string] [default: "%s"]
--outputTemplate Template for mapping output or "restricted" type names
[string] [default: "%s__Output"]
```

### Example Usage
Expand Down
84 changes: 55 additions & 29 deletions packages/proto-loader/bin/proto-loader-gen-types.ts
Expand Up @@ -26,12 +26,25 @@ import * as yargs from 'yargs';
import camelCase = require('lodash.camelcase');
import { loadProtosWithOptions, addCommonProtos } from '../src/util';

const templateStr = "%s";
const useNameFmter = ({outputTemplate, inputTemplate}: GeneratorOptions) => {
if (outputTemplate === inputTemplate) {
throw new Error('inputTemplate and outputTemplate must differ')
}
return {
outputName: (n: string) => outputTemplate.replace(templateStr, n),
inputName: (n: string) => inputTemplate.replace(templateStr, n)
};
}

type GeneratorOptions = Protobuf.IParseOptions & Protobuf.IConversionOptions & {
includeDirs?: string[];
grpcLib: string;
outDir: string;
verbose?: boolean;
includeComments?: boolean;
inputTemplate: string;
outputTemplate: string;
}

class TextFormatter {
Expand Down Expand Up @@ -114,15 +127,16 @@ function getTypeInterfaceName(type: Protobuf.Type | Protobuf.Enum | Protobuf.Ser
return type.fullName.replace(/\./g, '_');
}

function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Service, from?: Protobuf.Type | Protobuf.Service) {
function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Service, from: Protobuf.Type | Protobuf.Service | undefined, options: GeneratorOptions) {
const filePath = from === undefined ? './' + getImportPath(dependency) : getRelativeImportPath(from, dependency);
const {outputName, inputName} = useNameFmter(options);
const typeInterfaceName = getTypeInterfaceName(dependency);
let importedTypes: string;
/* If the dependency is defined within a message, it will be generated in that
* message's file and exported using its typeInterfaceName. */
if (dependency.parent instanceof Protobuf.Type) {
if (dependency instanceof Protobuf.Type) {
importedTypes = `${typeInterfaceName}, ${typeInterfaceName}__Output`;
importedTypes = `${inputName(typeInterfaceName)}, ${outputName(typeInterfaceName)}`;
} else if (dependency instanceof Protobuf.Enum) {
importedTypes = `${typeInterfaceName}`;
} else if (dependency instanceof Protobuf.Service) {
Expand All @@ -132,7 +146,7 @@ function getImportLine(dependency: Protobuf.Type | Protobuf.Enum | Protobuf.Serv
}
} else {
if (dependency instanceof Protobuf.Type) {
importedTypes = `${dependency.name} as ${typeInterfaceName}, ${dependency.name}__Output as ${typeInterfaceName}__Output`;
importedTypes = `${inputName(dependency.name)} as ${inputName(typeInterfaceName)}, ${outputName(dependency.name)} as ${outputName(typeInterfaceName)}`;
} else if (dependency instanceof Protobuf.Enum) {
importedTypes = `${dependency.name} as ${typeInterfaceName}`;
} else if (dependency instanceof Protobuf.Service) {
Expand Down Expand Up @@ -170,7 +184,8 @@ function formatComment(formatter: TextFormatter, comment?: string | null) {

// GENERATOR FUNCTIONS

function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean): string {
function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean, options: GeneratorOptions): string {
const {inputName} = useNameFmter(options);
switch (fieldType) {
case 'double':
case 'float':
Expand Down Expand Up @@ -200,18 +215,18 @@ function getTypeNamePermissive(fieldType: string, resolvedType: Protobuf.Type |
const typeInterfaceName = getTypeInterfaceName(resolvedType);
if (resolvedType instanceof Protobuf.Type) {
if (repeated || map) {
return typeInterfaceName;
return inputName(typeInterfaceName);
} else {
return `${typeInterfaceName} | null`;
return `${inputName(typeInterfaceName)} | null`;
}
} else {
return `${typeInterfaceName} | keyof typeof ${typeInterfaceName}`;
}
}
}

function getFieldTypePermissive(field: Protobuf.FieldBase): string {
const valueType = getTypeNamePermissive(field.type, field.resolvedType, field.repeated, field.map);
function getFieldTypePermissive(field: Protobuf.FieldBase, options: GeneratorOptions): string {
const valueType = getTypeNamePermissive(field.type, field.resolvedType, field.repeated, field.map, options);
if (field instanceof Protobuf.MapField) {
const keyType = field.keyType === 'string' ? 'string' : 'number';
return `{[key: ${keyType}]: ${valueType}}`;
Expand All @@ -221,23 +236,24 @@ function getFieldTypePermissive(field: Protobuf.FieldBase): string {
}

function generatePermissiveMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) {
const {inputName} = useNameFmter(options);
if (options.includeComments) {
formatComment(formatter, messageType.comment);
}
if (messageType.fullName === '.google.protobuf.Any') {
/* This describes the behavior of the Protobuf.js Any wrapper fromObject
* replacement function */
formatter.writeLine('export type Any = AnyExtension | {');
formatter.writeLine(`export type ${inputName('Any')} = AnyExtension | {`);
formatter.writeLine(' type_url: string;');
formatter.writeLine(' value: Buffer | Uint8Array | string;');
formatter.writeLine('}');
return;
}
formatter.writeLine(`export interface ${nameOverride ?? messageType.name} {`);
formatter.writeLine(`export interface ${inputName(nameOverride ?? messageType.name)} {`);
formatter.indent();
for (const field of messageType.fieldsArray) {
const repeatedString = field.repeated ? '[]' : '';
const type: string = getFieldTypePermissive(field);
const type: string = getFieldTypePermissive(field, options);
if (options.includeComments) {
formatComment(formatter, field.comment);
}
Expand All @@ -255,6 +271,7 @@ function generatePermissiveMessageInterface(formatter: TextFormatter, messageTyp
}

function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type | Protobuf.Enum | null, repeated: boolean, map: boolean, options: GeneratorOptions): string {
const {outputName} = useNameFmter(options);
switch (fieldType) {
case 'double':
case 'float':
Expand Down Expand Up @@ -302,9 +319,9 @@ function getTypeNameRestricted(fieldType: string, resolvedType: Protobuf.Type |
/* null is only used to represent absent message values if the defaults
* option is set, and only for non-repeated, non-map fields. */
if (options.defaults && !repeated && !map) {
return `${typeInterfaceName}__Output | null`;
return `${outputName(typeInterfaceName)} | null`;
} else {
return `${typeInterfaceName}__Output`;
return `${outputName(typeInterfaceName)}`;
}
} else {
if (options.enums == String) {
Expand All @@ -327,20 +344,21 @@ function getFieldTypeRestricted(field: Protobuf.FieldBase, options: GeneratorOpt
}

function generateRestrictedMessageInterface(formatter: TextFormatter, messageType: Protobuf.Type, options: GeneratorOptions, nameOverride?: string) {
const {outputName} = useNameFmter(options);
if (options.includeComments) {
formatComment(formatter, messageType.comment);
}
if (messageType.fullName === '.google.protobuf.Any' && options.json) {
/* This describes the behavior of the Protobuf.js Any wrapper toObject
* replacement function */
let optionalString = options.defaults ? '' : '?';
formatter.writeLine('export type Any__Output = AnyExtension | {');
formatter.writeLine(`export type ${outputName('Any')} = AnyExtension | {`);
formatter.writeLine(` type_url${optionalString}: string;`);
formatter.writeLine(` value${optionalString}: ${getTypeNameRestricted('bytes', null, false, false, options)};`);
formatter.writeLine('}');
return;
}
formatter.writeLine(`export interface ${nameOverride ?? messageType.name}__Output {`);
formatter.writeLine(`export interface ${outputName(nameOverride ?? messageType.name)} {`);
formatter.indent();
for (const field of messageType.fieldsArray) {
let fieldGuaranteed: boolean;
Expand Down Expand Up @@ -389,7 +407,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob
continue;
}
seenDeps.add(dependency.fullName);
formatter.writeLine(getImportLine(dependency, messageType));
formatter.writeLine(getImportLine(dependency, messageType, options));
}
if (field.type.indexOf('64') >= 0) {
usesLong = true;
Expand All @@ -404,7 +422,7 @@ function generateMessageInterfaces(formatter: TextFormatter, messageType: Protob
continue;
}
seenDeps.add(dependency.fullName);
formatter.writeLine(getImportLine(dependency, messageType));
formatter.writeLine(getImportLine(dependency, messageType, options));
}
if (field.type.indexOf('64') >= 0) {
usesLong = true;
Expand Down Expand Up @@ -487,6 +505,7 @@ const CLIENT_RESERVED_METHOD_NAMES = new Set([
]);

function generateServiceClientInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) {
const {outputName, inputName} = useNameFmter(options);
if (options.includeComments) {
formatComment(formatter, serviceType.comment);
}
Expand All @@ -501,8 +520,8 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P
if (options.includeComments) {
formatComment(formatter, method.comment);
}
const requestType = getTypeInterfaceName(method.resolvedRequestType!);
const responseType = getTypeInterfaceName(method.resolvedResponseType!) + '__Output';
const requestType = inputName(getTypeInterfaceName(method.resolvedRequestType!));
const responseType = outputName(getTypeInterfaceName(method.resolvedResponseType!));
const callbackType = `grpc.requestCallback<${responseType}>`;
if (method.requestStream) {
if (method.responseStream) {
Expand Down Expand Up @@ -541,6 +560,7 @@ function generateServiceClientInterface(formatter: TextFormatter, serviceType: P
}

function generateServiceHandlerInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) {
const {inputName, outputName} = useNameFmter(options);
if (options.includeComments) {
formatComment(formatter, serviceType.comment);
}
Expand All @@ -551,8 +571,8 @@ function generateServiceHandlerInterface(formatter: TextFormatter, serviceType:
if (options.includeComments) {
formatComment(formatter, method.comment);
}
const requestType = getTypeInterfaceName(method.resolvedRequestType!) + '__Output';
const responseType = getTypeInterfaceName(method.resolvedResponseType!);
const requestType = outputName(getTypeInterfaceName(method.resolvedRequestType!));
const responseType = inputName(getTypeInterfaceName(method.resolvedResponseType!));
if (method.requestStream) {
if (method.responseStream) {
// Bidi streaming
Expand All @@ -576,14 +596,15 @@ function generateServiceHandlerInterface(formatter: TextFormatter, serviceType:
formatter.writeLine('}');
}

function generateServiceDefinitionInterface(formatter: TextFormatter, serviceType: Protobuf.Service) {
function generateServiceDefinitionInterface(formatter: TextFormatter, serviceType: Protobuf.Service, options: GeneratorOptions) {
const {inputName, outputName} = useNameFmter(options);
formatter.writeLine(`export interface ${serviceType.name}Definition extends grpc.ServiceDefinition {`);
formatter.indent();
for (const methodName of Object.keys(serviceType.methods).sort()) {
const method = serviceType.methods[methodName];
const requestType = getTypeInterfaceName(method.resolvedRequestType!);
const responseType = getTypeInterfaceName(method.resolvedResponseType!);
formatter.writeLine(`${methodName}: MethodDefinition<${requestType}, ${responseType}, ${requestType}__Output, ${responseType}__Output>`);
formatter.writeLine(`${methodName}: MethodDefinition<${inputName(requestType)}, ${inputName(responseType)}, ${outputName(requestType)}, ${outputName(responseType)}>`);
}
formatter.unindent();
formatter.writeLine('}')
Expand All @@ -601,7 +622,7 @@ function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protob
dependencies.add(method.resolvedResponseType!);
}
for (const dep of Array.from(dependencies.values()).sort(compareName)) {
formatter.writeLine(getImportLine(dep, serviceType));
formatter.writeLine(getImportLine(dep, serviceType, options));
}
formatter.writeLine('');

Expand All @@ -611,7 +632,7 @@ function generateServiceInterfaces(formatter: TextFormatter, serviceType: Protob
generateServiceHandlerInterface(formatter, serviceType, options);
formatter.writeLine('');

generateServiceDefinitionInterface(formatter, serviceType);
generateServiceDefinitionInterface(formatter, serviceType, options);
}

function containsDefinition(definitionType: typeof Protobuf.Type | typeof Protobuf.Enum, namespace: Protobuf.NamespaceBase): boolean {
Expand Down Expand Up @@ -645,7 +666,7 @@ function generateDefinitionImports(formatter: TextFormatter, namespace: Protobuf
function generateServiceImports(formatter: TextFormatter, namespace: Protobuf.NamespaceBase, options: GeneratorOptions) {
for (const nested of namespace.nestedArray.sort(compareName)) {
if (nested instanceof Protobuf.Service) {
formatter.writeLine(getImportLine(nested));
formatter.writeLine(getImportLine(nested, undefined, options));
} else if (isNamespaceBase(nested) && !(nested instanceof Protobuf.Type) && !(nested instanceof Protobuf.Enum)) {
generateServiceImports(formatter, nested, options);
}
Expand Down Expand Up @@ -776,7 +797,7 @@ async function runScript() {
.normalize(['includeDirs', 'outDir'])
.array('includeDirs')
.boolean(['keepCase', 'defaults', 'arrays', 'objects', 'oneofs', 'json', 'verbose', 'includeComments'])
.string(['longs', 'enums', 'bytes'])
.string(['longs', 'enums', 'bytes', 'inputTemplate', 'outputTemplate'])
.default('keepCase', false)
.default('defaults', false)
.default('arrays', false)
Expand All @@ -787,6 +808,8 @@ async function runScript() {
.default('longs', 'Long')
.default('enums', 'number')
.default('bytes', 'Buffer')
.default('inputTemplate', `${templateStr}`)
.default('outputTemplate', `${templateStr}__Output`)
.coerce('longs', value => {
switch (value) {
case 'String': return String;
Expand All @@ -805,7 +828,8 @@ async function runScript() {
case 'String': return String;
default: return undefined;
}
}).alias({
})
.alias({
includeDirs: 'I',
outDir: 'O',
verbose: 'v'
Expand All @@ -822,7 +846,9 @@ async function runScript() {
includeComments: 'Generate doc comments from comments in the original files',
includeDirs: 'Directories to search for included files',
outDir: 'Directory in which to output files',
grpcLib: 'The gRPC implementation library that these types will be used with'
grpcLib: 'The gRPC implementation library that these types will be used with',
inputTemplate: 'Template for mapping input or "permissive" type names',
outputTemplate: 'Template for mapping output or "restricted" type names',
}).demandOption(['outDir', 'grpcLib'])
.demand(1)
.usage('$0 [options] filenames...')
Expand Down
Expand Up @@ -4,7 +4,7 @@
/**
* A custom pattern is used for defining custom HTTP verb.
*/
export interface CustomHttpPattern {
export interface ICustomHttpPattern {
/**
* The name of this custom HTTP verb.
*/
Expand All @@ -18,7 +18,7 @@ export interface CustomHttpPattern {
/**
* A custom pattern is used for defining custom HTTP verb.
*/
export interface CustomHttpPattern__Output {
export interface OCustomHttpPattern {
/**
* The name of this custom HTTP verb.
*/
Expand Down
10 changes: 5 additions & 5 deletions packages/proto-loader/golden-generated/google/api/Http.ts
@@ -1,19 +1,19 @@
// Original file: deps/googleapis/google/api/http.proto

import type { HttpRule as _google_api_HttpRule, HttpRule__Output as _google_api_HttpRule__Output } from '../../google/api/HttpRule';
import type { IHttpRule as I_google_api_HttpRule, OHttpRule as O_google_api_HttpRule } from '../../google/api/HttpRule';

/**
* Defines the HTTP configuration for an API service. It contains a list of
* [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
* to one or more HTTP REST API methods.
*/
export interface Http {
export interface IHttp {
/**
* A list of HTTP configuration rules that apply to individual API methods.
*
* **NOTE:** All service configuration rules follow "last one wins" order.
*/
'rules'?: (_google_api_HttpRule)[];
'rules'?: (I_google_api_HttpRule)[];
/**
* When set to true, URL path parameters will be fully URI-decoded except in
* cases of single segment matches in reserved expansion, where "%2F" will be
Expand All @@ -30,13 +30,13 @@ export interface Http {
* [HttpRule][google.api.HttpRule], each specifying the mapping of an RPC method
* to one or more HTTP REST API methods.
*/
export interface Http__Output {
export interface OHttp {
/**
* A list of HTTP configuration rules that apply to individual API methods.
*
* **NOTE:** All service configuration rules follow "last one wins" order.
*/
'rules': (_google_api_HttpRule__Output)[];
'rules': (O_google_api_HttpRule)[];
/**
* When set to true, URL path parameters will be fully URI-decoded except in
* cases of single segment matches in reserved expansion, where "%2F" will be
Expand Down

0 comments on commit 04b14eb

Please sign in to comment.