1// Copyright 2021 The Pigweed Authors 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); you may not 4// use this file except in compliance with the License. You may obtain a copy of 5// the License at 6// 7// https://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations under 13// the License. 14 15import { 16 MessageCreator, 17 ProtoCollection, 18} from 'pigweedjs/pw_protobuf_compiler'; 19import { Message } from 'google-protobuf'; 20import { 21 MethodDescriptorProto, 22 ServiceDescriptorProto, 23} from 'google-protobuf/google/protobuf/descriptor_pb'; 24 25import { hash } from './hash'; 26 27interface ChannelOutput { 28 (data: Uint8Array): void; 29} 30 31export class Channel { 32 readonly id: number; 33 private output: ChannelOutput; 34 35 constructor( 36 id: number, 37 output: ChannelOutput = () => { 38 /* do nothing. */ 39 }, 40 ) { 41 this.id = id; 42 this.output = output; 43 } 44 45 send(data: Uint8Array) { 46 this.output(data); 47 } 48} 49 50/** Describes an RPC service. */ 51export class Service { 52 readonly name: string; 53 readonly id: number; 54 readonly methods = new Map<number, Method>(); 55 readonly methodsByName = new Map<string, Method>(); 56 57 constructor(serviceName: string, methodsList: RPCMethodDescriptor[]) { 58 this.name = serviceName; 59 this.id = hash(this.name); 60 methodsList.forEach((methodDescriptor) => { 61 const method = new Method(this, methodDescriptor); 62 this.methods.set(method.id, method); 63 this.methodsByName.set(method.name, method); 64 }); 65 } 66 static fromProtoDescriptor( 67 descriptor: ServiceDescriptorProto, 68 protoCollection: ProtoCollection, 69 packageName: string, 70 ) { 71 const name = packageName + '.' + descriptor.getName()!; 72 const methodList = descriptor 73 .getMethodList() 74 .map((methodDescriptor: MethodDescriptorProto) => 75 Method.protoDescriptorToRPCMethodDescriptor( 76 methodDescriptor, 77 protoCollection, 78 ), 79 ); 80 return new Service(name, methodList); 81 } 82} 83 84export type MessageSerializer = { 85 serialize: (message: Message) => Uint8Array; 86 deserialize: (bytes: Uint8Array) => Message; 87}; 88 89export type RPCMethodDescriptor = { 90 name: string; 91 requestType: MessageCreator; 92 responseType: MessageCreator; 93 customRequestSerializer?: MessageSerializer; 94 customResponseSerializer?: MessageSerializer; 95 clientStreaming?: boolean; 96 serverStreaming?: boolean; 97 protoDescriptor?: MethodDescriptorProto; 98}; 99 100export enum MethodType { 101 UNARY, 102 SERVER_STREAMING, 103 CLIENT_STREAMING, 104 BIDIRECTIONAL_STREAMING, 105} 106 107/** Describes an RPC method. */ 108export class Method { 109 readonly service: Service; 110 readonly name: string; 111 readonly id: number; 112 readonly clientStreaming: boolean; 113 readonly serverStreaming: boolean; 114 readonly requestType: any; 115 readonly responseType: any; 116 readonly descriptor?: MethodDescriptorProto; 117 readonly customRequestSerializer?: MessageSerializer; 118 readonly customResponseSerializer?: MessageSerializer; 119 120 constructor(service: Service, methodDescriptor: RPCMethodDescriptor) { 121 this.name = methodDescriptor.name; 122 this.id = hash(this.name); 123 this.service = service; 124 this.serverStreaming = methodDescriptor.serverStreaming || false; 125 this.clientStreaming = methodDescriptor.clientStreaming || false; 126 127 this.requestType = methodDescriptor.requestType; 128 this.responseType = methodDescriptor.responseType; 129 this.descriptor = methodDescriptor.protoDescriptor; 130 this.customRequestSerializer = methodDescriptor.customRequestSerializer; 131 this.customResponseSerializer = methodDescriptor.customResponseSerializer; 132 } 133 134 static protoDescriptorToRPCMethodDescriptor( 135 descriptor: MethodDescriptorProto, 136 protoCollection: ProtoCollection, 137 ): RPCMethodDescriptor { 138 const requestTypePath = descriptor.getInputType()!; 139 const responseTypePath = descriptor.getOutputType()!; 140 141 // Remove leading period if it exists. 142 const requestType = protoCollection.getMessageCreator( 143 requestTypePath.replace(/^\./, ''), 144 )!; 145 const responseType = protoCollection.getMessageCreator( 146 responseTypePath.replace(/^\./, ''), 147 )!; 148 149 return { 150 name: descriptor.getName()!, 151 serverStreaming: descriptor.getServerStreaming()!, 152 clientStreaming: descriptor.getClientStreaming()!, 153 requestType: requestType, 154 responseType: responseType, 155 protoDescriptor: descriptor, 156 }; 157 } 158 159 static fromProtoDescriptor( 160 descriptor: MethodDescriptorProto, 161 protoCollection: ProtoCollection, 162 service: Service, 163 ) { 164 return new Method( 165 service, 166 Method.protoDescriptorToRPCMethodDescriptor(descriptor, protoCollection), 167 ); 168 } 169 170 get type(): MethodType { 171 if (this.clientStreaming && this.serverStreaming) { 172 return MethodType.BIDIRECTIONAL_STREAMING; 173 } else if (this.clientStreaming && !this.serverStreaming) { 174 return MethodType.CLIENT_STREAMING; 175 } else if (!this.clientStreaming && this.serverStreaming) { 176 return MethodType.SERVER_STREAMING; 177 } else if (!this.clientStreaming && !this.serverStreaming) { 178 return MethodType.UNARY; 179 } 180 throw Error('Unhandled streaming condition'); 181 } 182} 183