Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce ConnectRouter.rpc overload to not require full ServiceType #884

Closed
wants to merge 14 commits into from
87 changes: 87 additions & 0 deletions packages/connect/src/method-type.ts
Original file line number Diff line number Diff line change
@@ -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<ServiceType, "methods">;
}

/**
* A unary method: rpc (Input) returns (Output)
*/
export interface MethodTypeUnary<I extends Message<I>, O extends Message<O>>
extends mtShared,
MethodInfoUnary<I, O> {}

/**
* A server streaming method: rpc (Input) returns (stream Output)
*/
export interface MethodTypeServerStreaming<
I extends Message<I>,
O extends Message<O>,
> extends mtShared,
MethodInfoServerStreaming<I, O> {}

/**
* A client streaming method: rpc (stream Input) returns (Output)
*/
export interface MethodTypeClientStreaming<
I extends Message<I>,
O extends Message<O>,
> extends mtShared,
MethodInfoClientStreaming<I, O> {}

/**
* A method that streams bi-directionally: rpc (stream Input) returns (stream Output)
*/
export interface MethodTypeBiDiStreaming<
I extends Message<I>,
O extends Message<O>,
> extends mtShared,
MethodInfoBiDiStreaming<I, O> {}

/**
* MethodType represents a self-contained method type. It must contain
* references to the service that implements it.
paul-sachs marked this conversation as resolved.
Show resolved Hide resolved
*
* 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<I> = AnyMessage,
O extends Message<O> = AnyMessage,
> =
| MethodTypeUnary<I, O>
| MethodTypeServerStreaming<I, O>
| MethodTypeClientStreaming<I, O>
| MethodTypeBiDiStreaming<I, O>;
74 changes: 74 additions & 0 deletions packages/connect/src/router.spec.ts
Original file line number Diff line number Diff line change
@@ -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: {},
});
});
});
45 changes: 42 additions & 3 deletions packages/connect/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -52,17 +53,31 @@ 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<T extends ServiceType>(
service: T,
implementation: Partial<ServiceImpl<T>>,
options?: Partial<UniversalHandlerOptions>,
): this;
/**
* Provides implementation for a single RPC given service and associated method.
*/
rpc<M extends MethodInfo>(
service: ServiceType,
method: M,
impl: MethodImpl<M>,
options?: Partial<UniversalHandlerOptions>,
): this;
/**
* Provides implementation for a single RPC given a method type.
paul-sachs marked this conversation as resolved.
Show resolved Hide resolved
*/
rpc<M extends MethodType>(
method: M,
impl: MethodImpl<M>,
options?: Partial<UniversalHandlerOptions>,
): this;
paul-sachs marked this conversation as resolved.
Show resolved Hide resolved
}

/**
Expand Down Expand Up @@ -119,6 +134,7 @@ export function createConnectRouter(
): ConnectRouter {
const base = whichProtocols(routerOptions);
const handlers: UniversalHandler[] = [];

return {
handlers,
service(service, implementation, options) {
Expand All @@ -131,11 +147,34 @@ export function createConnectRouter(
);
return this;
},
rpc(service, method, implementation, options) {
const { protocols } = whichProtocols(options, base);
rpc(
serviceOrMethod: ServiceType | MethodType,
methodOrImpl: MethodInfo | MethodImpl<MethodInfo>,
implementationOrOptions?:
| MethodImpl<MethodInfo>
| Partial<UniversalHandlerOptions>,
options?: Partial<UniversalHandlerOptions>,
) {
let service: ServiceType;
let method: MethodInfo;
let impl: MethodImpl<MethodInfo>;
let opt: Partial<UniversalHandlerOptions> | undefined;
if ("typeName" in serviceOrMethod) {
service = serviceOrMethod;
method = methodOrImpl as MethodInfo;
impl = implementationOrOptions as MethodImpl<MethodInfo>;
opt = options;
} else {
service = { ...serviceOrMethod.service, methods: {} };
method = serviceOrMethod;
impl = methodOrImpl as MethodImpl<MethodInfo>;
opt = implementationOrOptions as Partial<UniversalHandlerOptions>;
}
const { protocols } = whichProtocols(opt, base);

handlers.push(
createUniversalMethodHandler(
createMethodImplSpec(service, method, implementation),
createMethodImplSpec(service, method, impl),
protocols,
),
);
Expand Down