ts-proto
Version:
> `ts-proto` transforms your `.proto` files into strongly-typed, idiomatic TypeScript files!
451 lines (450 loc) • 20.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.detectBatchMethod = exports.responseObservable = exports.responsePromise = exports.responseType = exports.requestType = exports.detectMapType = exports.toTypeName = exports.getEnumMethod = exports.messageToTypeName = exports.valueTypeName = exports.isEmptyType = exports.isValueType = exports.isTimestamp = exports.isMapType = exports.isLong = exports.isRepeated = exports.isWithinOneOfThatShouldBeUnion = exports.isWithinOneOf = exports.isEnum = exports.isMessage = exports.isBytes = exports.isPrimitive = exports.createTypeMap = exports.defaultValue = exports.packedType = exports.toReaderCall = exports.basicTypeName = exports.basicLongWireType = exports.basicWireType = void 0;
const pbjs_1 = require("../build/pbjs");
const ts_poet_1 = require("ts-poet");
const main_1 = require("./main");
const utils_1 = require("./utils");
const sourceInfo_1 = require("./sourceInfo");
const case_1 = require("./case");
var FieldDescriptorProto = pbjs_1.google.protobuf.FieldDescriptorProto;
/** Based on https://github.com/dcodeIO/protobuf.js/blob/master/src/types.js#L37. */
function basicWireType(type) {
switch (type) {
case FieldDescriptorProto.Type.TYPE_DOUBLE:
return 1;
case FieldDescriptorProto.Type.TYPE_FLOAT:
return 5;
case FieldDescriptorProto.Type.TYPE_INT32:
case FieldDescriptorProto.Type.TYPE_ENUM:
case FieldDescriptorProto.Type.TYPE_UINT32:
case FieldDescriptorProto.Type.TYPE_SINT32:
return 0;
case FieldDescriptorProto.Type.TYPE_FIXED32:
case FieldDescriptorProto.Type.TYPE_SFIXED32:
return 5;
case FieldDescriptorProto.Type.TYPE_INT64:
case FieldDescriptorProto.Type.TYPE_UINT64:
case FieldDescriptorProto.Type.TYPE_SINT64:
return 0;
case FieldDescriptorProto.Type.TYPE_FIXED64:
case FieldDescriptorProto.Type.TYPE_SFIXED64:
return 1;
case FieldDescriptorProto.Type.TYPE_BOOL:
return 0;
case FieldDescriptorProto.Type.TYPE_STRING:
case FieldDescriptorProto.Type.TYPE_BYTES:
return 2;
default:
throw new Error('Invalid type ' + type);
}
}
exports.basicWireType = basicWireType;
function basicLongWireType(type) {
switch (type) {
case FieldDescriptorProto.Type.TYPE_INT64:
case FieldDescriptorProto.Type.TYPE_UINT64:
case FieldDescriptorProto.Type.TYPE_SINT64:
return 0;
case FieldDescriptorProto.Type.TYPE_FIXED64:
case FieldDescriptorProto.Type.TYPE_SFIXED64:
return 1;
default:
return undefined;
}
}
exports.basicLongWireType = basicLongWireType;
/** Returns the type name without any repeated/required/etc. labels. */
function basicTypeName(typeMap, field, options, typeOptions = {}) {
switch (field.type) {
case FieldDescriptorProto.Type.TYPE_DOUBLE:
case FieldDescriptorProto.Type.TYPE_FLOAT:
case FieldDescriptorProto.Type.TYPE_INT32:
case FieldDescriptorProto.Type.TYPE_UINT32:
case FieldDescriptorProto.Type.TYPE_SINT32:
case FieldDescriptorProto.Type.TYPE_FIXED32:
case FieldDescriptorProto.Type.TYPE_SFIXED32:
return ts_poet_1.TypeNames.NUMBER;
case FieldDescriptorProto.Type.TYPE_INT64:
case FieldDescriptorProto.Type.TYPE_UINT64:
case FieldDescriptorProto.Type.TYPE_SINT64:
case FieldDescriptorProto.Type.TYPE_FIXED64:
case FieldDescriptorProto.Type.TYPE_SFIXED64:
// this handles 2^53, Long is only needed for 2^64; this is effectively pbjs's forceNumber
if (options.forceLong === main_1.LongOption.LONG) {
return ts_poet_1.TypeNames.anyType('Long*long');
}
else if (options.forceLong === main_1.LongOption.STRING) {
return ts_poet_1.TypeNames.STRING;
}
else {
return ts_poet_1.TypeNames.NUMBER;
}
case FieldDescriptorProto.Type.TYPE_BOOL:
return ts_poet_1.TypeNames.BOOLEAN;
case FieldDescriptorProto.Type.TYPE_STRING:
return ts_poet_1.TypeNames.STRING;
case FieldDescriptorProto.Type.TYPE_BYTES:
if (options.env === main_1.EnvOption.NODE) {
return ts_poet_1.TypeNames.BUFFER;
}
else {
return ts_poet_1.TypeNames.anyType('Uint8Array');
}
case FieldDescriptorProto.Type.TYPE_MESSAGE:
case FieldDescriptorProto.Type.TYPE_ENUM:
return messageToTypeName(typeMap, field.typeName, options, { ...typeOptions, repeated: isRepeated(field) });
default:
return ts_poet_1.TypeNames.anyType(field.typeName);
}
}
exports.basicTypeName = basicTypeName;
/** Returns the Reader method for the primitive's read/write call. */
function toReaderCall(field) {
switch (field.type) {
case FieldDescriptorProto.Type.TYPE_DOUBLE:
return 'double';
case FieldDescriptorProto.Type.TYPE_FLOAT:
return 'float';
case FieldDescriptorProto.Type.TYPE_INT32:
case FieldDescriptorProto.Type.TYPE_ENUM:
return 'int32';
case FieldDescriptorProto.Type.TYPE_UINT32:
return 'uint32';
case FieldDescriptorProto.Type.TYPE_SINT32:
return 'sint32';
case FieldDescriptorProto.Type.TYPE_FIXED32:
return 'fixed32';
case FieldDescriptorProto.Type.TYPE_SFIXED32:
return 'sfixed32';
case FieldDescriptorProto.Type.TYPE_INT64:
return 'int64';
case FieldDescriptorProto.Type.TYPE_UINT64:
return 'uint64';
case FieldDescriptorProto.Type.TYPE_SINT64:
return 'sint64';
case FieldDescriptorProto.Type.TYPE_FIXED64:
return 'fixed64';
case FieldDescriptorProto.Type.TYPE_SFIXED64:
return 'sfixed64';
case FieldDescriptorProto.Type.TYPE_BOOL:
return 'bool';
case FieldDescriptorProto.Type.TYPE_STRING:
return 'string';
case FieldDescriptorProto.Type.TYPE_BYTES:
return 'bytes';
default:
throw new Error(`Not a primitive field ${field}`);
}
}
exports.toReaderCall = toReaderCall;
function packedType(type) {
switch (type) {
case FieldDescriptorProto.Type.TYPE_DOUBLE:
return 1;
case FieldDescriptorProto.Type.TYPE_FLOAT:
return 5;
case FieldDescriptorProto.Type.TYPE_INT32:
case FieldDescriptorProto.Type.TYPE_ENUM:
case FieldDescriptorProto.Type.TYPE_UINT32:
case FieldDescriptorProto.Type.TYPE_SINT32:
return 0;
case FieldDescriptorProto.Type.TYPE_FIXED32:
case FieldDescriptorProto.Type.TYPE_SFIXED32:
return 5;
case FieldDescriptorProto.Type.TYPE_INT64:
case FieldDescriptorProto.Type.TYPE_UINT64:
case FieldDescriptorProto.Type.TYPE_SINT64:
return 0;
case FieldDescriptorProto.Type.TYPE_FIXED64:
case FieldDescriptorProto.Type.TYPE_SFIXED64:
return 1;
case FieldDescriptorProto.Type.TYPE_BOOL:
return 0;
default:
return undefined;
}
}
exports.packedType = packedType;
function defaultValue(typeMap, field, options) {
switch (field.type) {
case FieldDescriptorProto.Type.TYPE_DOUBLE:
case FieldDescriptorProto.Type.TYPE_FLOAT:
case FieldDescriptorProto.Type.TYPE_INT32:
case FieldDescriptorProto.Type.TYPE_UINT32:
case FieldDescriptorProto.Type.TYPE_SINT32:
case FieldDescriptorProto.Type.TYPE_FIXED32:
case FieldDescriptorProto.Type.TYPE_SFIXED32:
return 0;
case FieldDescriptorProto.Type.TYPE_ENUM:
// proto3 enforces enums starting at 0, however proto2 does not, so we have
// to probe and see if zero is an allowed value. If it's not, pick the first one.
// This is probably not great, but it's only used in fromJSON and fromPartial,
// and I believe the semantics of those in the proto2 world are generally undefined.
const enumProto = typeMap.get(field.typeName)[2];
const hasZero = enumProto.value.find((v) => v.number === 0);
return hasZero ? 0 : enumProto.value[0].number;
case FieldDescriptorProto.Type.TYPE_UINT64:
case FieldDescriptorProto.Type.TYPE_FIXED64:
if (options.forceLong === main_1.LongOption.LONG) {
return ts_poet_1.CodeBlock.of('%T.UZERO', 'Long*long');
}
else if (options.forceLong === main_1.LongOption.STRING) {
return '"0"';
}
else {
return 0;
}
case FieldDescriptorProto.Type.TYPE_INT64:
case FieldDescriptorProto.Type.TYPE_SINT64:
case FieldDescriptorProto.Type.TYPE_SFIXED64:
if (options.forceLong === main_1.LongOption.LONG) {
return ts_poet_1.CodeBlock.of('%T.ZERO', 'Long*long');
}
else if (options.forceLong === main_1.LongOption.STRING) {
return '"0"';
}
else {
return 0;
}
case FieldDescriptorProto.Type.TYPE_BOOL:
return false;
case FieldDescriptorProto.Type.TYPE_STRING:
return '""';
case FieldDescriptorProto.Type.TYPE_BYTES:
if (options.env === main_1.EnvOption.NODE) {
return 'new Buffer(0)';
}
else {
return 'new Uint8Array()';
}
case FieldDescriptorProto.Type.TYPE_MESSAGE:
default:
return 'undefined';
}
}
exports.defaultValue = defaultValue;
/** Scans all of the proto files in `request` and builds a map of proto typeName -> TS module/name. */
function createTypeMap(request, options) {
const typeMap = new Map();
for (const file of request.protoFile) {
// We assume a file.name of google/protobuf/wrappers.proto --> a module path of google/protobuf/wrapper.ts
const moduleName = file.name.replace('.proto', '');
// So given a fullName like FooMessage_InnerMessage, proto will see that as package.name.FooMessage.InnerMessage
function saveMapping(tsFullName, desc, s, protoFullName) {
// package is optional, but make sure we have a dot-prefixed type name either way
const prefix = file.package.length === 0 ? '' : `.${file.package}`;
typeMap.set(`${prefix}.${protoFullName}`, [moduleName, tsFullName, desc]);
}
main_1.visit(file, sourceInfo_1.default.empty(), saveMapping, options, saveMapping);
}
return typeMap;
}
exports.createTypeMap = createTypeMap;
function isPrimitive(field) {
return !isMessage(field);
}
exports.isPrimitive = isPrimitive;
function isBytes(field) {
return field.type === FieldDescriptorProto.Type.TYPE_BYTES;
}
exports.isBytes = isBytes;
function isMessage(field) {
return field.type === FieldDescriptorProto.Type.TYPE_MESSAGE;
}
exports.isMessage = isMessage;
function isEnum(field) {
return field.type === FieldDescriptorProto.Type.TYPE_ENUM;
}
exports.isEnum = isEnum;
function isWithinOneOf(field) {
return field.hasOwnProperty('oneofIndex');
}
exports.isWithinOneOf = isWithinOneOf;
function isWithinOneOfThatShouldBeUnion(options, field) {
return isWithinOneOf(field) && options.oneof === main_1.OneofOption.UNIONS && !field.proto3Optional;
}
exports.isWithinOneOfThatShouldBeUnion = isWithinOneOfThatShouldBeUnion;
function isRepeated(field) {
return field.label === FieldDescriptorProto.Label.LABEL_REPEATED;
}
exports.isRepeated = isRepeated;
function isLong(field) {
return basicLongWireType(field.type) !== undefined;
}
exports.isLong = isLong;
function isMapType(typeMap, messageDesc, field, options) {
return detectMapType(typeMap, messageDesc, field, options) !== undefined;
}
exports.isMapType = isMapType;
const valueTypes = {
'.google.protobuf.StringValue': ts_poet_1.TypeNames.STRING,
'.google.protobuf.Int32Value': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.Int64Value': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.UInt32Value': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.UInt64Value': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.BoolValue': ts_poet_1.TypeNames.BOOLEAN,
'.google.protobuf.DoubleValue': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.FloatValue': ts_poet_1.TypeNames.NUMBER,
'.google.protobuf.BytesValue': ts_poet_1.TypeNames.anyType('Uint8Array'),
};
const mappedTypes = {
'.google.protobuf.Timestamp': ts_poet_1.TypeNames.DATE,
};
function isTimestamp(field) {
return field.typeName === '.google.protobuf.Timestamp';
}
exports.isTimestamp = isTimestamp;
function isValueType(field) {
return field.typeName in valueTypes;
}
exports.isValueType = isValueType;
function isEmptyType(typeName) {
return typeName === '.google.protobuf.Empty';
}
exports.isEmptyType = isEmptyType;
function valueTypeName(field) {
if (!isValueType(field)) {
throw new Error('Type is not a valueType: ' + field.typeName);
}
return valueTypes[field.typeName];
}
exports.valueTypeName = valueTypeName;
/** Maps `.some_proto_namespace.Message` to a TypeName. */
function messageToTypeName(typeMap, protoType, options, typeOptions = {}) {
// Watch for the wrapper types `.google.protobuf.*Value`. If we're mapping
// them to basic built-in types, we union the type with undefined to
// indicate the value is optional. Exceptions:
// - If the field is repeated, values cannot be undefined.
// - If useOptionals=true, all non-scalar types are already optional
// properties, so there's no need for that union.
if (!typeOptions.keepValueType && protoType in valueTypes) {
let typeName = valueTypes[protoType];
if (!!typeOptions.repeated || options.useOptionals) {
return typeName;
}
return ts_poet_1.TypeNames.unionType(typeName, ts_poet_1.TypeNames.UNDEFINED);
}
// Look for other special prototypes like Timestamp that aren't technically wrapper types
if (!typeOptions.keepValueType && protoType in mappedTypes) {
return mappedTypes[protoType];
}
const [module, type] = toModuleAndType(typeMap, protoType);
return ts_poet_1.TypeNames.importedType(`${type}@./${module}`);
}
exports.messageToTypeName = messageToTypeName;
/** Breaks `.some_proto_namespace.Some.Message` into `['some_proto_namespace', 'Some_Message', Descriptor]. */
function toModuleAndType(typeMap, protoType) {
return typeMap.get(protoType) || utils_1.fail(`No type found for ${protoType}`);
}
function getEnumMethod(typeMap, enumProtoType, methodSuffix) {
const [module, type] = toModuleAndType(typeMap, enumProtoType);
return ts_poet_1.TypeNames.importedType(`${case_1.camelCase(type)}${methodSuffix}@./${module}`);
}
exports.getEnumMethod = getEnumMethod;
/** Return the TypeName for any field (primitive/message/etc.) as exposed in the interface. */
function toTypeName(typeMap, messageDesc, field, options) {
let type = basicTypeName(typeMap, field, options, { keepValueType: false });
if (isRepeated(field)) {
const mapType = detectMapType(typeMap, messageDesc, field, options);
if (mapType) {
const { keyType, valueType } = mapType;
return ts_poet_1.TypeNames.anonymousType(new ts_poet_1.Member(`[key: ${keyType}]`, valueType));
}
return ts_poet_1.TypeNames.arrayType(type);
}
if (isValueType(field)) {
// google.protobuf.*Value types are already unioned with `undefined`
// in messageToTypeName, so no need to consider them for that here.
return type;
}
// By default (useOptionals=false, oneof=properties), non-scalar fields
// outside oneofs and all fields within a oneof clause need to be unioned
// with `undefined` to indicate the value is optional.
//
// When useOptionals=true, non-scalar fields are translated to optional
// properties, so no need for the union with `undefined` here.
//
// When oneof=unions, we generate a single property for the entire `oneof`
// clause, spelling each option out inside a large type union. No need for
// union with `undefined` here, either.
if ((!isWithinOneOf(field) && isMessage(field) && !options.useOptionals) ||
(isWithinOneOf(field) && options.oneof === main_1.OneofOption.PROPERTIES) ||
(isWithinOneOf(field) && field.proto3Optional)) {
return ts_poet_1.TypeNames.unionType(type, ts_poet_1.TypeNames.UNDEFINED);
}
return type;
}
exports.toTypeName = toTypeName;
function detectMapType(typeMap, messageDesc, fieldDesc, options) {
var _a;
if (fieldDesc.label === FieldDescriptorProto.Label.LABEL_REPEATED &&
fieldDesc.type === FieldDescriptorProto.Type.TYPE_MESSAGE) {
const mapType = typeMap.get(fieldDesc.typeName)[2];
if (!((_a = mapType.options) === null || _a === void 0 ? void 0 : _a.mapEntry))
return undefined;
const keyType = toTypeName(typeMap, messageDesc, mapType.field[0], options);
// use basicTypeName because we don't need the '| undefined'
const valueType = basicTypeName(typeMap, mapType.field[1], options);
return { messageDesc: mapType, keyType, valueType };
}
return undefined;
}
exports.detectMapType = detectMapType;
function requestType(typeMap, methodDesc, options) {
let typeName = messageToTypeName(typeMap, methodDesc.inputType, options);
if (methodDesc.clientStreaming) {
return ts_poet_1.TypeNames.anyType('Observable@rxjs').param(typeName);
}
return typeName;
}
exports.requestType = requestType;
function responseType(typeMap, methodDesc, options) {
return messageToTypeName(typeMap, methodDesc.outputType, options);
}
exports.responseType = responseType;
function responsePromise(typeMap, methodDesc, options) {
return ts_poet_1.TypeNames.PROMISE.param(responseType(typeMap, methodDesc, options));
}
exports.responsePromise = responsePromise;
function responseObservable(typeMap, methodDesc, options) {
return ts_poet_1.TypeNames.anyType('Observable@rxjs').param(responseType(typeMap, methodDesc, options));
}
exports.responseObservable = responseObservable;
function detectBatchMethod(typeMap, fileDesc, serviceDesc, methodDesc, options) {
const nameMatches = methodDesc.name.startsWith('Batch');
const inputType = typeMap.get(methodDesc.inputType);
const outputType = typeMap.get(methodDesc.outputType);
if (nameMatches && inputType && outputType) {
// TODO: This might be enums?
const inputTypeDesc = inputType[2];
const outputTypeDesc = outputType[2];
if (hasSingleRepeatedField(inputTypeDesc) && hasSingleRepeatedField(outputTypeDesc)) {
const singleMethodName = methodDesc.name.replace('Batch', 'Get');
const inputFieldName = inputTypeDesc.field[0].name;
const inputType = basicTypeName(typeMap, inputTypeDesc.field[0], options); // e.g. repeated string -> string
const outputFieldName = outputTypeDesc.field[0].name;
let outputType = basicTypeName(typeMap, outputTypeDesc.field[0], options); // e.g. repeated Entity -> Entity
const mapType = detectMapType(typeMap, outputTypeDesc, outputTypeDesc.field[0], options);
if (mapType) {
outputType = mapType.valueType;
}
const uniqueIdentifier = `${fileDesc.package}.${serviceDesc.name}.${methodDesc.name}`;
return {
methodDesc,
uniqueIdentifier,
singleMethodName,
inputFieldName,
inputType,
outputFieldName,
outputType,
mapType: !!mapType,
};
}
}
return undefined;
}
exports.detectBatchMethod = detectBatchMethod;
function hasSingleRepeatedField(messageDesc) {
return messageDesc.field.length == 1 && messageDesc.field[0].label === FieldDescriptorProto.Label.LABEL_REPEATED;
}