diff --git a/integration/grpc-web/example-test.ts b/integration/grpc-web/example-test.ts index ae6cff407..7509cdae1 100644 --- a/integration/grpc-web/example-test.ts +++ b/integration/grpc-web/example-test.ts @@ -1,4 +1,5 @@ import { DashStateClientImpl } from './example'; +import { EMPTY } from 'rxjs'; describe('grpc-web', () => { it('at least compiles', () => { @@ -19,5 +20,14 @@ describe('grpc-web', () => { const client = new DashStateClientImpl(rpc); const userSettings = client.UserSettings; userSettings({}); - }) + }); + it('throws on client streaming call', () => { + const rpc = { + unary: jest.fn(), + invoke: jest.fn(), + }; + const client = new DashStateClientImpl(rpc); + const call = () => client.ChangeUserSettingsStream(EMPTY) + expect(call).toThrowError("ts-proto does not yet support client streaming!") + }); }); diff --git a/integration/grpc-web/example.bin b/integration/grpc-web/example.bin index ace3893cc..72f331cf3 100644 Binary files a/integration/grpc-web/example.bin and b/integration/grpc-web/example.bin differ diff --git a/integration/grpc-web/example.proto b/integration/grpc-web/example.proto index 62ba9421d..f5ffd02b2 100644 --- a/integration/grpc-web/example.proto +++ b/integration/grpc-web/example.proto @@ -5,6 +5,8 @@ package rpx; service DashState { rpc UserSettings(Empty) returns (DashUserSettingsState); rpc ActiveUserSettingsStream(Empty) returns (stream DashUserSettingsState); + // not supported in grpc-web, but should still compile + rpc ChangeUserSettingsStream (stream DashUserSettingsState) returns (stream DashUserSettingsState) {} } message DashFlash { diff --git a/integration/grpc-web/example.ts b/integration/grpc-web/example.ts index feae85e4d..33a159660 100644 --- a/integration/grpc-web/example.ts +++ b/integration/grpc-web/example.ts @@ -595,6 +595,11 @@ export const Empty = { export interface DashState { UserSettings(request: DeepPartial, metadata?: grpc.Metadata): Promise; ActiveUserSettingsStream(request: DeepPartial, metadata?: grpc.Metadata): Observable; + /** not supported in grpc-web, but should still compile */ + ChangeUserSettingsStream( + request: Observable>, + metadata?: grpc.Metadata + ): Observable; } export class DashStateClientImpl implements DashState { @@ -604,6 +609,7 @@ export class DashStateClientImpl implements DashState { this.rpc = rpc; this.UserSettings = this.UserSettings.bind(this); this.ActiveUserSettingsStream = this.ActiveUserSettingsStream.bind(this); + this.ChangeUserSettingsStream = this.ChangeUserSettingsStream.bind(this); } UserSettings(request: DeepPartial, metadata?: grpc.Metadata): Promise { @@ -613,6 +619,13 @@ export class DashStateClientImpl implements DashState { ActiveUserSettingsStream(request: DeepPartial, metadata?: grpc.Metadata): Observable { return this.rpc.invoke(DashStateActiveUserSettingsStreamDesc, Empty.fromPartial(request), metadata); } + + ChangeUserSettingsStream( + request: Observable>, + metadata?: grpc.Metadata + ): Observable { + throw new Error('ts-proto does not yet support client streaming!'); + } } export const DashStateDesc = { diff --git a/src/generate-grpc-web.ts b/src/generate-grpc-web.ts index 0ffbc2f96..79c7376e9 100644 --- a/src/generate-grpc-web.ts +++ b/src/generate-grpc-web.ts @@ -1,5 +1,5 @@ import { MethodDescriptorProto, FileDescriptorProto, ServiceDescriptorProto } from 'ts-proto-descriptors'; -import { requestType, responsePromiseOrObservable, responseType } from './types'; +import { rawRequestType, requestType, responsePromiseOrObservable, responseType } from './types'; import { Code, code, imp, joinCode } from 'ts-poet'; import { Context } from './context'; import { assertInstanceOf, FormattedMethodDescriptor, maybePrefixPackage } from './utils'; @@ -35,7 +35,7 @@ export function generateGrpcClientImpl( assertInstanceOf(methodDesc, FormattedMethodDescriptor); chunks.push(code`this.${methodDesc.formattedName} = this.${methodDesc.formattedName}.bind(this);`); } - chunks.push(code`}\n`); + chunks.push(code`}`); // Create a method for each FooService method for (const methodDesc of serviceDesc.method) { @@ -43,25 +43,36 @@ export function generateGrpcClientImpl( } chunks.push(code`}`); - return joinCode(chunks, { trim: false }); + return joinCode(chunks, { trim: false, on: '\n' }); } /** Creates the RPC methods that client code actually calls. */ function generateRpcMethod(ctx: Context, serviceDesc: ServiceDescriptorProto, methodDesc: MethodDescriptorProto) { assertInstanceOf(methodDesc, FormattedMethodDescriptor); - const { options, utils } = ctx; - const inputType = requestType(ctx, methodDesc); - const partialInputType = code`${utils.DeepPartial}<${inputType}>`; + const requestMessage = rawRequestType(ctx, methodDesc); + const inputType = requestType(ctx, methodDesc, true); const returns = responsePromiseOrObservable(ctx, methodDesc); + + if (methodDesc.clientStreaming) { + return code` + ${methodDesc.formattedName}( + request: ${inputType}, + metadata?: grpc.Metadata, + ): ${returns} { + throw new Error('ts-proto does not yet support client streaming!'); + } + `; + } + const method = methodDesc.serverStreaming ? 'invoke' : 'unary'; return code` ${methodDesc.formattedName}( - request: ${partialInputType}, + request: ${inputType}, metadata?: grpc.Metadata, ): ${returns} { return this.rpc.${method}( ${methodDescName(serviceDesc, methodDesc)}, - ${inputType}.fromPartial(request), + ${requestMessage}.fromPartial(request), metadata, ); } diff --git a/src/generate-services.ts b/src/generate-services.ts index b7001695f..7b92fb87a 100644 --- a/src/generate-services.ts +++ b/src/generate-services.ts @@ -51,12 +51,10 @@ export function generateService( params.push(code`ctx: Context`); } - let inputType = requestType(ctx, methodDesc); // the grpc-web clients auto-`fromPartial` the input before handing off to grpc-web's // serde runtime, so it's okay to accept partial results from the client - if (options.outputClientImpl === 'grpc-web') { - inputType = code`${utils.DeepPartial}<${inputType}>`; - } + const partialInput = options.outputClientImpl === 'grpc-web'; + const inputType = requestType(ctx, methodDesc, partialInput); params.push(code`request: ${inputType}`); // Use metadata as last argument for interface only configuration diff --git a/src/main.ts b/src/main.ts index 9d1abdf63..a12c03764 100644 --- a/src/main.ts +++ b/src/main.ts @@ -231,7 +231,9 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri chunks.push(generateGrpcClientImpl(ctx, fileDesc, serviceDesc)); chunks.push(generateGrpcServiceDesc(fileDesc, serviceDesc)); serviceDesc.method.forEach((method) => { - chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method)); + if (!method.clientStreaming) { + chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method)); + } if (method.serverStreaming) { hasServerStreamingMethods = true; } diff --git a/src/types.ts b/src/types.ts index 00d00ccad..728a37f84 100644 --- a/src/types.ts +++ b/src/types.ts @@ -652,8 +652,13 @@ export function rawRequestType(ctx: Context, methodDesc: MethodDescriptorProto): return messageToTypeName(ctx, methodDesc.inputType); } -export function requestType(ctx: Context, methodDesc: MethodDescriptorProto): Code { +export function requestType(ctx: Context, methodDesc: MethodDescriptorProto, partial: boolean = false): Code { let typeName = rawRequestType(ctx, methodDesc); + + if (partial) { + typeName = code`${ctx.utils.DeepPartial}<${typeName}>`; + } + if (methodDesc.clientStreaming) { return code`${imp('Observable@rxjs')}<${typeName}>`; }