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: framework-agnostic service definitions #316

Merged
merged 6 commits into from
Jun 28, 2021
Merged
Show file tree
Hide file tree
Changes from 4 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
1 change: 1 addition & 0 deletions integration/service-definitions/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
outputServices=true
Binary file added integration/service-definitions/simple.bin
Binary file not shown.
25 changes: 25 additions & 0 deletions integration/service-definitions/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;
}
151 changes: 151 additions & 0 deletions integration/service-definitions/simple.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/* eslint-disable */
import { util, configure, Writer, Reader } from 'protobufjs/minimal';
import * as Long from 'long';

export const protobufPackage = 'simple';

export interface TestMessage {
value: string;
}

const baseTestMessage: object = { 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 = { ...baseTestMessage } as TestMessage;
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 {
const message = { ...baseTestMessage } as TestMessage;
if (object.value !== undefined && object.value !== null) {
message.value = String(object.value);
} else {
message.value = '';
}
return message;
},

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

fromPartial(object: DeepPartial<TestMessage>): TestMessage {
const message = { ...baseTestMessage } as TestMessage;
if (object.value !== undefined && object.value !== null) {
message.value = object.value;
} else {
message.value = '';
}
return message;
},
};

/** @deprecated */
export const TestDefinition = {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added the Definition suffix as per discussion.

Though now it seems that the suffix should be ServiceDefinition

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey @stephenh, thank you for review. What do you think about this one?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After some thinking, I'm fine with Definition suffix as long as we don't overload this term and only use it for services.

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>;

// 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();
}
2 changes: 1 addition & 1 deletion integration/update-bins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
# proto requests into .bin files that then unit tests can pull in directly as needed.
list=$(find . -name "*.proto" -type f)

for file in $list; do
for file in service-definitions/*.proto; do
echo "${file}"
# # Strip the longest suffix starting at the 1st slash
dir="${file##./}"
Expand Down
89 changes: 89 additions & 0 deletions src/generate-service-definition.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { Code, code, def, joinCode } from 'ts-poet';
import {
FileDescriptorProto,
MethodDescriptorProto,
MethodOptions,
MethodOptions_IdempotencyLevel,
ServiceDescriptorProto,
} from 'ts-proto-descriptors';
import { camelCase } from './case';
import { Context } from './context';
import SourceInfo, { Fields } from './sourceInfo';
import { messageToTypeName } from './types';
import { maybeAddComment, maybePrefixPackage } from './utils';

/**
* Generates a framework-agnostic service descriptor.
*/
export function generateServiceDefinition(
ctx: Context,
fileDesc: FileDescriptorProto,
sourceInfo: SourceInfo,
serviceDesc: ServiceDescriptorProto
) {
const chunks: Code[] = [];

maybeAddComment(sourceInfo, chunks, serviceDesc.options?.deprecated);

// Service definition
chunks.push(code`
export const ${def(`${serviceDesc.name}Definition`)} = {
`);

serviceDesc.options?.uninterpretedOption;
chunks.push(code`
name: '${serviceDesc.name}',
fullName: '${maybePrefixPackage(fileDesc, serviceDesc.name)}',
methods: {
`);

for (const [index, methodDesc] of serviceDesc.method.entries()) {
const info = sourceInfo.lookup(Fields.service.method, index);
maybeAddComment(info, chunks, methodDesc.options?.deprecated);

chunks.push(code`
${camelCase(methodDesc.name)}: ${generateMethodDefinition(ctx, methodDesc)},
`);
}

chunks.push(code`
},
} as const;
`);

return joinCode(chunks, { on: '\n' });
}

function generateMethodDefinition(ctx: Context, methodDesc: MethodDescriptorProto) {
const inputType = messageToTypeName(ctx, methodDesc.inputType, { keepValueType: true });
const outputType = messageToTypeName(ctx, methodDesc.outputType, { keepValueType: true });

return code`
{
name: '${methodDesc.name}',
requestType: ${inputType},
requestStream: ${methodDesc.clientStreaming},
responseType: ${outputType},
responseStream: ${methodDesc.serverStreaming},
options: ${generateMethodOptions(methodDesc.options)}
}
`;
}

function generateMethodOptions(options: MethodOptions | undefined) {
const chunks: Code[] = [];

chunks.push(code`{`);

if (options != null) {
if (options.idempotencyLevel === MethodOptions_IdempotencyLevel.IDEMPOTENT) {
chunks.push(code`idempotencyLevel: 'IDEMPOTENT',`);
} else if (options.idempotencyLevel === MethodOptions_IdempotencyLevel.NO_SIDE_EFFECTS) {
chunks.push(code`idempotencyLevel: 'NO_SIDE_EFFECTS',`);
}
}

chunks.push(code`}`);

return joinCode(chunks, { on: '\n' });
}
3 changes: 3 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ import { Context } from './context';
import { generateSchema } from './schema';
import { ConditionalOutput } from 'ts-poet/build/ConditionalOutput';
import { generateGrpcJsService } from './generate-grpc-js';
import { generateServiceDefinition } from './generate-service-definition';

export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [string, Code] {
const { options, utils } = ctx;
Expand Down Expand Up @@ -176,6 +177,8 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
chunks.push(code`export const ${serviceConstName} = "${serviceDesc.name}";`);
} else if (options.outputServices === 'grpc-js') {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (options.outputServices === true) {
chunks.push(generateServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else {
// 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.
Expand Down
2 changes: 1 addition & 1 deletion src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export type Options = {
stringEnums: boolean;
constEnums: boolean;
outputClientImpl: boolean | 'grpc-web';
outputServices: false | 'grpc-js';
outputServices: boolean | 'grpc-js';
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Need to add documentation on the option to the Readme

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

addGrpcMetadata: boolean;
addNestjsRestParameter: boolean;
returnObservable: boolean;
Expand Down