Skip to main content
Module

x/pbkit/compat/protoc/file-descriptor-set.ts

Protobuf toolkit for modern web development
Go to Latest
File
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566
import { Enum, File, Message, MessageField, Options, OptionValue, Schema, Service, Type,} from "../../core/schema/model.ts";import { DescriptorProto, EnumDescriptorProto, EnumValueOptions, FieldDescriptorProto, FileDescriptorProto, FileDescriptorSet, MethodDescriptorProto, ServiceDescriptorProto, UninterpretedOption,} from "../../generated/messages/google/protobuf/index.ts";import { Label as FieldLabel, Type as FieldType,} from "../../generated/messages/google/protobuf/(FieldDescriptorProto)/index.ts";import { snakeToCamel, snakeToPascal } from "../../misc/case.ts";import { Type as OptimizeMode,} from "../../generated/messages/google/protobuf/(FileOptions)/OptimizeMode.ts";import { Type as IdempotencyLevel,} from "../../generated/messages/google/protobuf/(MethodOptions)/IdempotencyLevel.ts";import { Type as CType } from "../../generated/messages/google/protobuf/(FieldOptions)/CType.ts";import { Type as JSType } from "../../generated/messages/google/protobuf/(FieldOptions)/JSType.ts";
export interface ConvertSchemaToFileDescriptorSetConfig { schema: Schema;}export function convertSchemaToFileDescriptorSet( { schema }: ConvertSchemaToFileDescriptorSetConfig,): FileDescriptorSet { const result: FileDescriptorSet = { file: [] }; for (const [filePath, file] of Object.entries(schema.files)) { const typeTreeArray = typePathsToTypeTreeArray( schema.types, file.typePaths, ); const messageTypes = typeTreeArray.filter(isMessageTypeTree); const enumTypes = typeTreeArray.filter(isEnumTypeTree); const services = getServicesFromFile(schema, file); const options = fileDescriptorOptions(file.options); const fileDescriptorProto: FileDescriptorProto = { name: file.importPath, package: file.package, dependency: file.imports.map(({ importPath }) => importPath), messageType: messageTypes.map((messageType) => convertMessageToDescriptorProto({ schema, filePath, messageType }) ), enumType: enumTypes.map(convertEnumToEnumDescriptorProto), service: services.map(convertServiceToDescriptorProto), extension: [], // TODO options: optionsOrUndefined(options), sourceCodeInfo: undefined, // TODO publicDependency: file.imports.map( ({ kind }, index) => kind === "public" ? index : -1, ).filter((v) => v >= 0), weakDependency: [], // TODO syntax: file.syntax, }; result.file.push(fileDescriptorProto); } return result;}
function convertServiceToDescriptorProto( service: ServiceWithName,): ServiceDescriptorProto { const methodDescriptorProtos: ServiceDescriptorProto["method"] = []; for (const [rpcName, rpc] of Object.entries(service.rpcs)) { const options = methodDescriptorOptions(rpc.options); const methodDescriptorProto: MethodDescriptorProto = { name: rpcName, inputType: rpc.reqType.typePath, outputType: rpc.resType.typePath, options: optionsOrUndefined(options), clientStreaming: rpc.reqType.stream, serverStreaming: rpc.resType.stream, }; methodDescriptorProtos.push(methodDescriptorProto); } return { name: service.name, method: methodDescriptorProtos, };}
interface ConvertMessageToDescriptorProtoConfig { schema: Schema; filePath: string; messageType: TypeTree<Message>;}function convertMessageToDescriptorProto( config: ConvertMessageToDescriptorProtoConfig,): DescriptorProto { const { schema, filePath, messageType } = config; const name = messageType.baseName; const nestedTypes = messageType.children.filter(isMessageTypeTree); const enumTypes = messageType.children.filter(isEnumTypeTree); const fieldDescriptorProtos: DescriptorProto["field"] = []; const oneofMap = new Map<string, number>(); for (const [fieldNumber, field] of Object.entries(messageType.type.fields)) { if (field.kind === "map") { const options = fieldDescriptorOptions(field.options); const entryTypeName = `${snakeToPascal(field.name)}Entry`; const entryTypePath = `${messageType.typePath}.${entryTypeName}`; const fieldDescriptorProto: FieldDescriptorProto = { name: field.name, extendee: undefined, // TODO number: Number(fieldNumber), label: getFieldLabel(field), type: getFieldType(schema, field), typeName: entryTypePath, defaultValue: "default" in field.options ? String(field.options["default"]) : undefined, options: optionsOrUndefined(options), oneofIndex: undefined, jsonName: field.options["json_name"]?.toString() ?? snakeToCamel(field.name), proto3Optional: undefined, // TODO }; fieldDescriptorProtos.push(fieldDescriptorProto); nestedTypes.push({ baseName: entryTypeName, typePath: entryTypePath, type: { kind: "message", filePath, description: { leading: [], trailing: [], leadingDetached: [], }, options: { "map_entry": true, }, fields: { 1: { kind: "normal", name: "key", type: field.keyType, typePath: field.keyTypePath, options: {}, description: { leading: [], trailing: [], leadingDetached: [], }, }, 2: { kind: "normal", name: "value", type: field.valueType, typePath: field.valueTypePath, options: {}, description: { leading: [], trailing: [], leadingDetached: [], }, }, }, groups: {}, reservedFieldNumberRanges: [], reservedFieldNames: [], extensions: [], }, children: [], }); continue; } const options = fieldDescriptorOptions(field.options); const fieldDescriptorProto: FieldDescriptorProto = { name: field.name, extendee: undefined, // TODO number: Number(fieldNumber), label: getFieldLabel(field), type: getFieldType(schema, field), typeName: field.typePath || field.type, defaultValue: "default" in field.options ? String(field.options["default"]) : undefined, options: optionsOrUndefined(options), oneofIndex: undefined, jsonName: field.options["json_name"]?.toString() ?? snakeToCamel(field.name), proto3Optional: undefined, // TODO }; if (field.kind === "oneof") { if (!oneofMap.has(field.oneof)) oneofMap.set(field.oneof, oneofMap.size); fieldDescriptorProto.oneofIndex = oneofMap.get(field.oneof); } fieldDescriptorProtos.push(fieldDescriptorProto); } const oneofDecl: DescriptorProto["oneofDecl"] = Array.from( oneofMap.keys(), ).map( (name) => ({ name }), ); const options = descriptorOptions(messageType.type.options); return { name, field: fieldDescriptorProtos, nestedType: nestedTypes.map( (messageType) => convertMessageToDescriptorProto({ schema, filePath, messageType }), ), enumType: enumTypes.map(convertEnumToEnumDescriptorProto), extensionRange: [], // TODO extension: [], // TODO options: optionsOrUndefined(options), oneofDecl, reservedRange: [], // TODO reservedName: [], // TODO };}
function getFieldLabel(field: MessageField): FieldLabel { switch (field.kind) { case "normal": case "oneof": case "optional": return "LABEL_OPTIONAL"; case "required": return "LABEL_REQUIRED"; case "map": case "repeated": return "LABEL_REPEATED"; default: return "UNSPECIFIED"; }}
function getFieldType(schema: Schema, field: MessageField): FieldType { if (field.kind === "map") return "TYPE_MESSAGE"; // TODO if (!field.typePath) return "UNSPECIFIED"; if (schema.types[field.typePath]) { switch (schema.types[field.typePath].kind) { case "message": return "TYPE_MESSAGE"; case "enum": return "TYPE_ENUM"; } } switch (field.typePath) { case ".double": return "TYPE_DOUBLE"; case ".float": return "TYPE_FLOAT"; case ".int64": return "TYPE_INT64"; case ".uint64": return "TYPE_UINT64"; case ".int32": return "TYPE_INT32"; case ".fixed64": return "TYPE_FIXED64"; case ".fixed32": return "TYPE_FIXED32"; case ".bool": return "TYPE_BOOL"; case ".string": return "TYPE_STRING"; case ".bytes": return "TYPE_BYTES"; case ".uint32": return "TYPE_UINT32"; case ".sfixed32": return "TYPE_SFIXED32"; case ".sfixed64": return "TYPE_SFIXED64"; case ".sint32": return "TYPE_SINT32"; case ".sint64": return "TYPE_SINT64"; default: return "UNSPECIFIED"; }}
function convertEnumToEnumDescriptorProto( enumType: TypeTree<Enum>,): EnumDescriptorProto { const name = enumType.baseName; const value: EnumDescriptorProto["value"] = []; for (const [fieldNumber, field] of Object.entries(enumType.type.fields)) { const name = field.name; const number = Number(fieldNumber); const options = enumValueOptions(field.options); value.push({ name, number, options: optionsOrUndefined(options) }); } const options = enumDescriptorOptions(enumType.type.options); return { name, value, options: optionsOrUndefined(options), reservedRange: [], // TODO reservedName: [], // TODO };}
interface TypeTree<T extends Type = Type> { baseName: string; typePath: string; type: T; children: TypeTree[];}function typePathsToTypeTreeArray( types: Schema["types"], typePaths: string[],): TypeTree[] { const result: TypeTree[] = []; const typePathSet = new Set(typePaths); const roots = new Set<string>(); const childrenTable: { [typePath: string]: string[] /* childTypePaths */; } = {}; for (const typePath of typePathSet) { const parentTypePath = getParentTypePath(typePath); if (!parentTypePath || !typePathSet.has(parentTypePath)) { roots.add(typePath); continue; } const children = childrenTable[parentTypePath] ||= []; children.push(typePath); } function typePathToTypeTree(typePath: string): TypeTree { const type = types[typePath]; const baseName = getBaseName(typePath); const children = childrenTable[typePath]?.map( (childTypePath) => typePathToTypeTree(childTypePath), ) ?? []; return { baseName, typePath, type, children }; } for (const root of roots) result.push(typePathToTypeTree(root)); return result;}
function getBaseName(typePath: string): string { const lastDotIndex = typePath.lastIndexOf("."); if (lastDotIndex < 0) return typePath; return typePath.slice(lastDotIndex + 1);}
function getParentTypePath(typePath: string): string | undefined { const lastDotIndex = typePath.lastIndexOf("."); if (lastDotIndex < 1) return undefined; return typePath.slice(0, lastDotIndex);}
function isMessageTypeTree(typeTree: TypeTree): typeTree is TypeTree<Message> { return isMessageType(typeTree.type);}
function isEnumTypeTree(typeTree: TypeTree): typeTree is TypeTree<Enum> { return isEnumType(typeTree.type);}
function isMessageType<T extends Type>(type: Type): type is T { return type.kind === "message";}
function isEnumType<T extends Type>(type: Type): type is T { return type.kind === "enum";}
interface ServiceWithName extends Service { name: string;}function getServicesFromFile(schema: Schema, file: File): ServiceWithName[] { const result: ServiceWithName[] = []; for (const servicePath of file.servicePaths) { if (!(servicePath in schema.services)) continue; result.push({ ...schema.services[servicePath], name: servicePath.split(".").pop() || "", }); } return result;}
type OptionsBase = { uninterpretedOption: UninterpretedOption[] };function optionsOrUndefined<T extends OptionsBase>(options: T): T | undefined { const { uninterpretedOption, ...rest } = options; const countOfOption = ( uninterpretedOption.length + Object.values(rest).filter((x) => x !== undefined).length ); return countOfOption === 0 ? undefined : options;}
function booleanOptionValue( value: OptionValue | undefined,): boolean | undefined { return value !== undefined ? Boolean(value) : value;}
type RequiredOptional<T extends { [key: string]: any }> = { [key in keyof Required<T>]-?: T[key];};function fileDescriptorOptions( options: Options,): RequiredOptional<NonNullable<FileDescriptorProto["options"]>> { const result: RequiredOptional<NonNullable<FileDescriptorProto["options"]>> = { javaPackage: options["java_package"]?.toString(), javaOuterClassname: options["java_outer_classname"]?.toString(), javaMultipleFiles: booleanOptionValue( options["java_multiple_files"], ), goPackage: options["go_package"]?.toString(), ccGenericServices: booleanOptionValue( options["cc_generic_services"], ), javaGenericServices: booleanOptionValue( options["java_generic_services"], ), pyGenericServices: booleanOptionValue( options["py_generic_services"], ), javaGenerateEqualsAndHash: booleanOptionValue( options["java_generate_equals_and_hash"], ), deprecated: booleanOptionValue(options["deprecated"]), javaStringCheckUtf8: booleanOptionValue( options["java_string_check_utf8"], ), ccEnableArenas: booleanOptionValue(options["cc_enable_arenas"]), objcClassPrefix: options["objc_class_prefix"]?.toString(), csharpNamespace: options["csharp_namespace"]?.toString(), swiftPrefix: options["swift_prefix"]?.toString(), phpClassPrefix: options["php_class_prefix"]?.toString(), phpNamespace: options["php_namespace"]?.toString(), phpGenericServices: booleanOptionValue( options["php_generic_services"], ), phpMetadataNamespace: options["php_metadata_namespace"]?.toString(), rubyPackage: options["ruby_package"]?.toString(), optimizeFor: getOptimizeMode(options["optimize_for"]), uninterpretedOption: [], // TODO }; return result; function getOptimizeMode(optimizeFor: OptionValue): OptimizeMode | undefined { switch (optimizeFor) { case "SPEED": case "CODE_SIZE": case "LITE_RUNTIME": return optimizeFor; default: return undefined; } }}
function methodDescriptorOptions( options: Options,): RequiredOptional<NonNullable<MethodDescriptorProto["options"]>> { const result: RequiredOptional< NonNullable<MethodDescriptorProto["options"]> > = { deprecated: booleanOptionValue(options["deprecated"]), idempotencyLevel: getIdempotencyLevel(options["idempotency_level"]), uninterpretedOption: [], // TODO }; return result; function getIdempotencyLevel( idempotencyLevel: OptionValue, ): IdempotencyLevel | undefined { switch (idempotencyLevel) { case "IDEMPOTENCY_UNKNOWN": case "NO_SIDE_EFFECTS": case "IDEMPOTENT": return idempotencyLevel; default: return undefined; } }}
function fieldDescriptorOptions( options: Options,): RequiredOptional<NonNullable<FieldDescriptorProto["options"]>> { const result: RequiredOptional<NonNullable<FieldDescriptorProto["options"]>> = { packed: booleanOptionValue(options["packed"]), deprecated: booleanOptionValue(options["deprecated"]), lazy: booleanOptionValue(options["lazy"]), weak: booleanOptionValue(options["weak"]), unverifiedLazy: booleanOptionValue(options["unverified_lazy"]), ctype: getCType(options["ctype"]), jstype: getJSType(options["jstype"]), uninterpretedOption: [], // TODO }; return result; function getCType(ctype: OptionValue): CType | undefined { switch (ctype) { case "STRING": case "CORD": case "STRING_PIECE": return ctype; default: return undefined; } } function getJSType(jstype: OptionValue): JSType | undefined { switch (jstype) { case "JS_NORMAL": case "JS_STRING": case "JS_NUMBER": return jstype; default: return undefined; } }}
function enumDescriptorOptions( options: Options,): RequiredOptional<NonNullable<EnumDescriptorProto["options"]>> { const result: RequiredOptional<NonNullable<EnumDescriptorProto["options"]>> = { allowAlias: booleanOptionValue(options["allow_alias"]), deprecated: booleanOptionValue(options["deprecated"]), uninterpretedOption: [], // TODO }; return result;}
function enumValueOptions( options: Options,): RequiredOptional<EnumValueOptions> { const result: RequiredOptional<EnumValueOptions> = { deprecated: booleanOptionValue(options["deprecated"]), uninterpretedOption: [], // TODO }; return result;}
function descriptorOptions( options: Options,): RequiredOptional<NonNullable<DescriptorProto["options"]>> { const result: RequiredOptional<NonNullable<DescriptorProto["options"]>> = { messageSetWireFormat: booleanOptionValue( options["message_set_wire_format"], ), noStandardDescriptorAccessor: booleanOptionValue( options["no_standard_descriptor_accessor"], ), deprecated: booleanOptionValue(options["deprecated"]), mapEntry: booleanOptionValue(options["map_entry"]), uninterpretedOption: [], // TODO }; return result;}