xref: /aosp_15_r20/external/pigweed/pw_rpc/ts/descriptors.ts (revision 61c4878ac05f98d0ceed94b57d316916de578985)
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