diff --git a/packages/connect/src/method-type.ts b/packages/connect/src/method-type.ts new file mode 100644 index 000000000..f99f6137c --- /dev/null +++ b/packages/connect/src/method-type.ts @@ -0,0 +1,87 @@ +// Copyright 2021-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import type { + AnyMessage, + Message, + MethodInfoBiDiStreaming, + MethodInfoClientStreaming, + MethodInfoServerStreaming, + MethodInfoUnary, + ServiceType, +} from "@bufbuild/protobuf"; + +interface mtShared { + readonly service: Omit; +} + +/** + * A unary method: rpc (Input) returns (Output) + */ +export interface MethodTypeUnary, O extends Message> + extends mtShared, + MethodInfoUnary {} + +/** + * A server streaming method: rpc (Input) returns (stream Output) + */ +export interface MethodTypeServerStreaming< + I extends Message, + O extends Message, +> extends mtShared, + MethodInfoServerStreaming {} + +/** + * A client streaming method: rpc (stream Input) returns (Output) + */ +export interface MethodTypeClientStreaming< + I extends Message, + O extends Message, +> extends mtShared, + MethodInfoClientStreaming {} + +/** + * A method that streams bi-directionally: rpc (stream Input) returns (stream Output) + */ +export interface MethodTypeBiDiStreaming< + I extends Message, + O extends Message, +> extends mtShared, + MethodInfoBiDiStreaming {} + +/** + * MethodType represents a self-contained method type. It must contain + * references to the service that implements it. + * + * This type should ultimately live inside @bufbuild/protobuf but will + * exist here for now until https://github.com/bufbuild/protobuf-es/pull/594 + * can be merged/resolved. + * + * - "name": The original name of the protobuf rpc. + * - "I": The input message type. + * - "O": The output message type. + * - "kind": The method type. + * - "idempotency": User-provided indication whether the method will cause + * the same effect every time it is called. + * - "localName": The local name of the method, safe to use in ECMAScript. + * - "service": The service that implements the method, without methods. + */ +export type MethodType< + I extends Message = AnyMessage, + O extends Message = AnyMessage, +> = + | MethodTypeUnary + | MethodTypeServerStreaming + | MethodTypeClientStreaming + | MethodTypeBiDiStreaming; diff --git a/packages/connect/src/router.spec.ts b/packages/connect/src/router.spec.ts new file mode 100644 index 000000000..fa7fc338b --- /dev/null +++ b/packages/connect/src/router.spec.ts @@ -0,0 +1,74 @@ +// Copyright 2021-2023 The Connect Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +import { + Int32Value, + MethodIdempotency, + MethodKind, + StringValue, +} from "@bufbuild/protobuf"; +import { createConnectRouter } from "./router.js"; + +const testService = { + typeName: "TestService", + methods: { + unary: { + name: "Unary", + I: Int32Value, + O: StringValue, + kind: MethodKind.Unary, + }, + server: { + name: "Server", + I: Int32Value, + O: StringValue, + kind: MethodKind.ServerStreaming, + }, + client: { + name: "Client", + I: Int32Value, + O: StringValue, + kind: MethodKind.ClientStreaming, + }, + biDi: { + name: "BiDi", + I: Int32Value, + O: StringValue, + kind: MethodKind.BiDiStreaming, + }, + }, +} as const; + +describe("createConnectRouter", function () { + it("supports self describing method type", function () { + const methodDefinition = { + name: "Unary", + kind: MethodKind.Unary, + I: Int32Value, + O: StringValue, + service: testService, + idempotency: MethodIdempotency.NoSideEffects, + } as const; + const router = createConnectRouter({}).rpc(methodDefinition, (request) => { + return { value: `${request.value}-RESPONSE` }; + }); + + expect(router.handlers).toHaveSize(1); + expect(router.handlers[0].method).toEqual(methodDefinition); + expect(router.handlers[0].service).toEqual({ + ...testService, + methods: {}, + }); + }); +}); diff --git a/packages/connect/src/router.ts b/packages/connect/src/router.ts index 7a8077dc2..5ef42b4fd 100644 --- a/packages/connect/src/router.ts +++ b/packages/connect/src/router.ts @@ -33,6 +33,7 @@ import type { UniversalHandlerOptions, } from "./protocol/universal-handler.js"; import type { ProtocolHandlerFactory } from "./protocol/protocol-handler-factory.js"; +import type { MethodType } from "./method-type.js"; /** * ConnectRouter is your single registration point for RPCs. @@ -52,17 +53,33 @@ import type { ProtocolHandlerFactory } from "./protocol/protocol-handler-factory */ export interface ConnectRouter { readonly handlers: UniversalHandler[]; + /** + * Provides implementation for a set of RPCs on the service. + */ service( service: T, implementation: Partial>, options?: Partial, ): this; + /** + * Provides implementation for a single RPC given service and associated method. + */ rpc( service: ServiceType, method: M, impl: MethodImpl, options?: Partial, ): this; + /** + * Provides implementation for a single RPC given a method type. + * + * @private This is an experimental API. Please do not rely on it yet. + */ + rpc( + method: M, + impl: MethodImpl, + options?: Partial, + ): this; } /** @@ -119,6 +136,7 @@ export function createConnectRouter( ): ConnectRouter { const base = whichProtocols(routerOptions); const handlers: UniversalHandler[] = []; + return { handlers, service(service, implementation, options) { @@ -131,11 +149,34 @@ export function createConnectRouter( ); return this; }, - rpc(service, method, implementation, options) { - const { protocols } = whichProtocols(options, base); + rpc( + serviceOrMethod: ServiceType | MethodType, + methodOrImpl: MethodInfo | MethodImpl, + implementationOrOptions?: + | MethodImpl + | Partial, + options?: Partial, + ) { + let service: ServiceType; + let method: MethodInfo; + let impl: MethodImpl; + let opt: Partial | undefined; + if ("typeName" in serviceOrMethod) { + service = serviceOrMethod; + method = methodOrImpl as MethodInfo; + impl = implementationOrOptions as MethodImpl; + opt = options; + } else { + service = { ...serviceOrMethod.service, methods: {} }; + method = serviceOrMethod; + impl = methodOrImpl as MethodImpl; + opt = implementationOrOptions as Partial; + } + const { protocols } = whichProtocols(opt, base); + handlers.push( createUniversalMethodHandler( - createMethodImplSpec(service, method, implementation), + createMethodImplSpec(service, method, impl), protocols, ), );