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

feat: Allow simultaneous services and generic service definitions #512

Merged
merged 19 commits into from
Mar 4, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,8 @@ Generated code will be placed in the Gradle build directory.

- With `--ts_proto_opt=outputServices=generic-definitions`, ts-proto will output generic (framework-agnostic) service definitions. These definitions contain descriptors for each method with links to request and response types, which allows to generate server and client stubs at runtime, and also generate strong types for them at compile time. An example of a library that uses this approach is [nice-grpc](https://github.com/deeplay-io/nice-grpc).

- With `--ts_proto_opt=outputServices=generic-definitions,outputServices=default`, ts-proto will output both generic definitions and interfaces. This is useful if you want to rely on the interfaces, but also have some reflection capabilities at runtime.

- With `--ts_proto_opt=outputServices=false`, or `=none`, ts-proto will output NO service definitions.

- With `--ts_proto_opt=emitImportedFiles=false`, ts-proto will not emit `google/protobuf/*` files unless you explicit add files to `protoc` like this
Expand Down
Binary file modified integration/fieldmask/fieldmask.bin
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputServices=generic-definitions,outputServices=generic-definitions=default
Binary file not shown.
25 changes: 25 additions & 0 deletions integration/generic-service-definitions-and-services/simple.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
syntax = "proto3";

package simple;

service Test {
option deprecated = true;

rpc Unary (TestMessage) returns (TestMessage) {}
rpc ServerStreaming (TestMessage) returns (stream TestMessage) {}
rpc ClientStreaming (stream TestMessage) returns (TestMessage) {}
rpc BidiStreaming (stream TestMessage) returns (stream TestMessage) {}
rpc Deprecated (TestMessage) returns (TestMessage) {
option deprecated = true;
}
rpc Idempotent (TestMessage) returns (TestMessage) {
option idempotency_level = IDEMPOTENT;
}
rpc NoSideEffects (TestMessage) returns (TestMessage) {
option idempotency_level = NO_SIDE_EFFECTS;
}
}

message TestMessage {
string value = 1;
}
155 changes: 155 additions & 0 deletions integration/generic-service-definitions-and-services/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
/* eslint-disable */
import { util, configure, Writer, Reader } from 'protobufjs/minimal';
import * as Long from 'long';

export const protobufPackage = 'simple';

export interface TestMessage {
value: string;
}

function createBaseTestMessage(): TestMessage {
return { value: '' };
}

export const TestMessage = {
encode(message: TestMessage, writer: Writer = Writer.create()): Writer {
if (message.value !== '') {
writer.uint32(10).string(message.value);
}
return writer;
},

decode(input: Reader | Uint8Array, length?: number): TestMessage {
const reader = input instanceof Reader ? input : new Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = createBaseTestMessage();
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.value = reader.string();
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},

fromJSON(object: any): TestMessage {
return {
value: isSet(object.value) ? String(object.value) : '',
};
},

toJSON(message: TestMessage): unknown {
const obj: any = {};
message.value !== undefined && (obj.value = message.value);
return obj;
},

fromPartial<I extends Exact<DeepPartial<TestMessage>, I>>(object: I): TestMessage {
const message = createBaseTestMessage();
message.value = object.value ?? '';
return message;
},
};

/** @deprecated */
export const TestDefinition = {
name: 'Test',
fullName: 'simple.Test',
methods: {
unary: {
name: 'Unary',
requestType: TestMessage,
requestStream: false,
responseType: TestMessage,
responseStream: false,
options: {},
},
serverStreaming: {
name: 'ServerStreaming',
requestType: TestMessage,
requestStream: false,
responseType: TestMessage,
responseStream: true,
options: {},
},
clientStreaming: {
name: 'ClientStreaming',
requestType: TestMessage,
requestStream: true,
responseType: TestMessage,
responseStream: false,
options: {},
},
bidiStreaming: {
name: 'BidiStreaming',
requestType: TestMessage,
requestStream: true,
responseType: TestMessage,
responseStream: true,
options: {},
},
/** @deprecated */
deprecated: {
name: 'Deprecated',
requestType: TestMessage,
requestStream: false,
responseType: TestMessage,
responseStream: false,
options: {},
},
idempotent: {
name: 'Idempotent',
requestType: TestMessage,
requestStream: false,
responseType: TestMessage,
responseStream: false,
options: {
idempotencyLevel: 'IDEMPOTENT',
},
},
noSideEffects: {
name: 'NoSideEffects',
requestType: TestMessage,
requestStream: false,
responseType: TestMessage,
responseStream: false,
options: {
idempotencyLevel: 'NO_SIDE_EFFECTS',
},
},
},
} as const;

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;

// If you get a compile-error about 'Constructor<Long> and ... have no overlap',
// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.
if (util.Long !== Long) {
util.Long = Long as any;
configure();
}

function isSet(value: any): boolean {
return value !== null && value !== undefined;
}
51 changes: 30 additions & 21 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,27 +212,32 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
}

chunks.push(code`export const ${serviceConstName} = "${serviceDesc.name}";`);
} else if (options.outputServices === ServiceOption.GRPC) {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (options.outputServices === ServiceOption.GENERIC) {
chunks.push(generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else if (options.outputServices === ServiceOption.DEFAULT) {
// This service could be Twirp or grpc-web or JSON (maybe). So far all of their
// interfaces are fairly similar so we share the same service interface.
chunks.push(generateService(ctx, fileDesc, sInfo, serviceDesc));

if (options.outputClientImpl === true) {
chunks.push(generateServiceClientImpl(ctx, fileDesc, serviceDesc));
} else if (options.outputClientImpl === 'grpc-web') {
chunks.push(generateGrpcClientImpl(ctx, fileDesc, serviceDesc));
chunks.push(generateGrpcServiceDesc(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method));
if (method.serverStreaming) {
hasServerStreamingMethods = true;
} else {
const uniqueServices = [...new Set(options.outputServices)].sort();
uniqueServices.forEach((outputService) => {
if (outputService === ServiceOption.GRPC) {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.GENERIC) {
chunks.push(generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else if (outputService === ServiceOption.DEFAULT) {
// This service could be Twirp or grpc-web or JSON (maybe). So far all of their
// interfaces are fairly similar so we share the same service interface.
chunks.push(generateService(ctx, fileDesc, sInfo, serviceDesc));

if (options.outputClientImpl === true) {
chunks.push(generateServiceClientImpl(ctx, fileDesc, serviceDesc));
} else if (options.outputClientImpl === 'grpc-web') {
chunks.push(generateGrpcClientImpl(ctx, fileDesc, serviceDesc));
chunks.push(generateGrpcServiceDesc(fileDesc, serviceDesc));
serviceDesc.method.forEach((method) => {
chunks.push(generateGrpcMethodDesc(ctx, serviceDesc, method));
if (method.serverStreaming) {
hasServerStreamingMethods = true;
}
});
}
});
}
}
});
}
serviceDesc.method.forEach((methodDesc, index) => {
if (methodDesc.serverStreaming || methodDesc.clientStreaming) {
Expand All @@ -241,7 +246,11 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
});
});

if (options.outputServices === ServiceOption.DEFAULT && options.outputClientImpl && fileDesc.service.length > 0) {
if (
options.outputServices.includes(ServiceOption.DEFAULT) &&
options.outputClientImpl &&
fileDesc.service.length > 0
) {
if (options.outputClientImpl === true) {
chunks.push(generateRpcType(ctx, hasStreamingMethods));
} else if (options.outputClientImpl === 'grpc-web') {
Expand Down
15 changes: 12 additions & 3 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ export type Options = {
constEnums: boolean;
enumsAsLiterals: boolean;
outputClientImpl: boolean | 'grpc-web';
outputServices: ServiceOption;
outputServices: ServiceOption[];
addGrpcMetadata: boolean;
addNestjsRestParameter: boolean;
returnObservable: boolean;
Expand Down Expand Up @@ -84,7 +84,7 @@ export function defaultOptions(): Options {
constEnums: false,
enumsAsLiterals: false,
outputClientImpl: true,
outputServices: ServiceOption.DEFAULT,
outputServices: [],
returnObservable: false,
addGrpcMetadata: false,
addNestjsRestParameter: false,
Expand Down Expand Up @@ -131,7 +131,16 @@ export function optionsFromParameter(parameter: string | undefined): Options {

// Treat outputServices=false as NONE
if ((options.outputServices as any) === false) {
options.outputServices = ServiceOption.NONE;
options.outputServices = [ServiceOption.NONE];
}

// Existing type-coercion inside parseParameter leaves a little to be desired.
if (typeof options.outputServices == 'string') {
options.outputServices = [options.outputServices];
}

if (options.outputServices.length == 0) {
options.outputServices = [ServiceOption.DEFAULT];
}

if ((options.useDate as any) === true) {
Expand Down
2 changes: 1 addition & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto {
public static formatName(methodName: string, options: Options) {
let result = methodName;

if (options.lowerCaseServiceMethods || options.outputServices === ServiceOption.GRPC) {
if (options.lowerCaseServiceMethods || options.outputServices.includes(ServiceOption.GRPC)) {
result = camelCase(result);
}

Expand Down
8 changes: 5 additions & 3 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ describe('options', () => {
"outputJsonMethods": true,
"outputPartialMethods": false,
"outputSchema": false,
"outputServices": "default",
"outputServices": Array [
"default",
],
"outputTypeRegistry": false,
"returnObservable": false,
"snakeToCamel": Array [
Expand Down Expand Up @@ -61,14 +63,14 @@ describe('options', () => {
it('can set outputServices to false', () => {
const options = optionsFromParameter('outputServices=false');
expect(options).toMatchObject({
outputServices: ServiceOption.NONE,
outputServices: [ServiceOption.NONE],
});
});

it('can set outputServices to grpc', () => {
const options = optionsFromParameter('outputServices=grpc-js');
expect(options).toMatchObject({
outputServices: ServiceOption.GRPC,
outputServices: [ServiceOption.GRPC],
});
});

Expand Down