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: Service generation option #357

Merged
merged 6 commits into from
Sep 12, 2021
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
18 changes: 10 additions & 8 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,7 @@ Generated code will be placed in the Gradle build directory.
The default behavior is `forceLong=number`, which will internally still use the `long` library to encode/decode values on the wire (so you will still see a `util.Long = Long` line in your output), but will convert the `long` values to `number` automatically for you. Note that a runtime error is thrown if, while doing this conversion, a 64-bit value is larger than can be correctly stored as a `number`.

- With `--ts_proto_opt=esModuleInterop=true` changes output to be `esModuleInterop` compliant.

Specifically the `Long` imports will be generated as `import Long from 'long'` instead of `import * as Long from 'long'`.

- With `--ts_proto_opt=env=node` or `browser` or `both`, ts-proto will make environment-specific assumptions in your output. This defaults to `both`, which makes no environment-specific assumptions.
Expand Down Expand Up @@ -259,7 +259,7 @@ Generated code will be placed in the Gradle build directory.
```

However, the type-safety of `useOptionals=false` is admittedly tedious if you have many inherently-unused fields, so you can use `useOptionals=true` if that trade-off makes sense for your project.

You can also use the generated `SomeMessage.fromPartial` methods to opt into the optionality on a per-call-site basis. The `fromPartial` allows the creator/writer to have default values applied (i.e. `undefined` --> `0`), and the return value will still be the non-optional type that provides a consistent view (i.e. always `0`) to clients.

Eventually if TypesCript supports [Exact Types](https://github.com/microsoft/TypeScript/issues/12936), that should allow ts-proto to switch to `useOptionals=true` as the default/only behavior, have the generated `Message.encode`/`Message.toPartial`/etc. methods accept `Exact<T>` versions of the message types, and the result would be both safe + succinct.
Expand All @@ -269,10 +269,10 @@ Generated code will be placed in the Gradle build directory.
Note that RPC methods, like `service.ping({ key: ... })`, accept `DeepPartial` versions of the request messages, because of the same rationale that it makes it easy for the writer call-site to get default values for free, and because the "reader" is the internal ts-proto serialization code, it can apply the defaults as necessary.

- With `--ts_proto_opt=exportCommonSymbols=false`, utility types like `DeepPartial` won't be `export`d.

This should make it possible to use create barrel imports of the generated output, i.e. `import * from ./foo` and `import * from ./bar`.
Note that if you have the same message name used in multiple `*.proto` files, you will still get import conflicts.

Note that if you have the same message name used in multiple `*.proto` files, you will still get import conflicts.

- With `--ts_proto_opt=oneof=unions`, `oneof` fields will be generated as ADTs.

Expand Down Expand Up @@ -336,6 +336,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.

- 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
`protoc --plugin=./node_modules/.bin/protoc-gen-ts_proto my_message.proto google/protobuf/duration.proto`

Expand Down Expand Up @@ -410,7 +412,7 @@ If you want fields where you can model set/unset, see Wrapper Types.
# Wrapper Types

In core Protobuf, unset primitive fields become their respective default values (so you loose ability to distinguish "unset" from "default").

However, unset message fields stay `null`.

This allows a cute hack where you can model a logical `string | unset` by creating a field that is technically a message (i.e. so it can stay `null` for the unset case), but the message only has a single string field (i.e for storing the value in the set case).
Expand All @@ -426,9 +428,9 @@ This makes dealing with `string | unset` in your code much nicer, albeit it's un
Numbers are by default assumed to be plain JavaScript `number`s.

This is fine for Protobuf types like `int32` and `float`, but 64-bit types like `int64` can't be 100% represented by JavaScript's `number` type, because `int64` can have larger/smaller values than `number`.

ts-proto's default configuration (which is `forceLong=number`) is to still use `number` for 64-bit fields, and then throw an error if a value (at runtime) is larger than `Number.MAX_SAFE_INTEGER`.

If you expect to use 64-bit / higher-than-`MAX_SAFE_INTEGER` values, then you can use the ts-proto `forceLong` option, which uses the [long](https://www.npmjs.com/package/long) npm package to support the entire range of 64-bit values.

The protobuf number types map to JavaScript types based on the `forceLong` config option:
Expand Down
5 changes: 3 additions & 2 deletions src/generate-nestjs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { contextTypeVar } from './main';
import { assertInstanceOf, FormattedMethodDescriptor, maybeAddComment, singular } from './utils';
import { camelCase } from './case';
import { Context } from './context';
import { ServiceOption } from './options';

export function generateNestjsServiceController(
ctx: Context,
Expand All @@ -23,7 +24,7 @@ export function generateNestjsServiceController(
const { options } = ctx;
const chunks: Code[] = [];

const Metadata = options.outputServices === 'grpc-js' ? imp('Metadata@@grpc/grpc-js') : imp('Metadata@grpc');
const Metadata = options.outputServices === ServiceOption.GRPC ? imp('Metadata@@grpc/grpc-js') : imp('Metadata@grpc');

maybeAddComment(sourceInfo, chunks, serviceDesc.options?.deprecated);
const t = options.context ? `<${contextTypeVar}>` : '';
Expand Down Expand Up @@ -96,7 +97,7 @@ export function generateNestjsServiceClient(
const { options } = ctx;
const chunks: Code[] = [];

const Metadata = options.outputServices === 'grpc-js' ? imp('Metadata@@grpc/grpc-js') : imp('Metadata@grpc');
const Metadata = options.outputServices === ServiceOption.GRPC ? imp('Metadata@@grpc/grpc-js') : imp('Metadata@grpc');

maybeAddComment(sourceInfo, chunks);
const t = options.context ? `<${contextTypeVar}>` : ``;
Expand Down
10 changes: 5 additions & 5 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ import {
} from './generate-grpc-web';
import { generateEnum } from './enums';
import { visit, visitServices } from './visit';
import { EnvOption, LongOption, OneofOption, Options, DateOption } from './options';
import { EnvOption, LongOption, OneofOption, Options, ServiceOption, DateOption } from './options';
import { Context } from './context';
import { generateSchema } from './schema';
import { ConditionalOutput } from 'ts-poet/build/ConditionalOutput';
Expand Down Expand Up @@ -183,11 +183,11 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
}

chunks.push(code`export const ${serviceConstName} = "${serviceDesc.name}";`);
} else if (options.outputServices === 'grpc-js') {
} else if (options.outputServices === ServiceOption.GRPC) {
chunks.push(generateGrpcJsService(ctx, fileDesc, sInfo, serviceDesc));
} else if (options.outputServices === 'generic-definitions') {
} else if (options.outputServices === ServiceOption.GENERIC) {
chunks.push(generateGenericServiceDefinition(ctx, fileDesc, sInfo, serviceDesc));
} else {
} 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));
Expand All @@ -207,7 +207,7 @@ export function generateFile(ctx: Context, fileDesc: FileDescriptorProto): [stri
}
});

if (!options.outputServices && options.outputClientImpl && fileDesc.service.length > 0) {
if (options.outputServices === ServiceOption.DEFAULT && options.outputClientImpl && fileDesc.service.length > 0) {
if (options.outputClientImpl === true) {
chunks.push(generateRpcType(ctx));
} else if (options.outputClientImpl === 'grpc-web') {
Expand Down
16 changes: 14 additions & 2 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ export enum OneofOption {
UNIONS = 'unions',
}

export enum ServiceOption {
GRPC = 'grpc-js',
GENERIC = 'generic-definitions',
DEFAULT = 'default',
NONE = 'none',
}

export type Options = {
context: boolean;
snakeToCamel: boolean;
Expand All @@ -36,7 +43,7 @@ export type Options = {
stringEnums: boolean;
constEnums: boolean;
outputClientImpl: boolean | 'grpc-web';
outputServices: false | 'grpc-js' | 'generic-definitions';
outputServices: ServiceOption;
addGrpcMetadata: boolean;
addNestjsRestParameter: boolean;
returnObservable: boolean;
Expand Down Expand Up @@ -68,7 +75,7 @@ export function defaultOptions(): Options {
stringEnums: false,
constEnums: false,
outputClientImpl: true,
outputServices: false,
outputServices: ServiceOption.DEFAULT,
returnObservable: false,
addGrpcMetadata: false,
addNestjsRestParameter: false,
Expand Down Expand Up @@ -109,6 +116,11 @@ export function optionsFromParameter(parameter: string): Options {
options.forceLong = LongOption.LONG;
}

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

if ((options.useDate as any) === true) {
// Treat useDate=true as DATE
options.useDate = DateOption.DATE;
Expand Down
4 changes: 2 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { code, Code } from 'ts-poet';
import { CodeGeneratorRequest, FileDescriptorProto, MethodDescriptorProto, MethodOptions } from 'ts-proto-descriptors';
import ReadStream = NodeJS.ReadStream;
import { SourceDescription } from './sourceInfo';
import { Options } from './options';
import { Options, ServiceOption } from './options';
import { camelCase } from './case';

export function protoFilesToGenerate(request: CodeGeneratorRequest): FileDescriptorProto[] {
Expand Down Expand Up @@ -160,7 +160,7 @@ export class FormattedMethodDescriptor implements MethodDescriptorProto {
public static formatName(methodName: string, options: Options) {
let result = methodName;

if (options.lowerCaseServiceMethods || options.outputServices === 'grpc-js') {
if (options.lowerCaseServiceMethods || options.outputServices === ServiceOption.GRPC) {
result = camelCase(result);
}

Expand Down
18 changes: 16 additions & 2 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { optionsFromParameter } from '../src/options';
import { optionsFromParameter, ServiceOption } from '../src/options';

describe('options', () => {
it('can set outputJsonMethods with nestJs=true', () => {
Expand All @@ -22,7 +22,7 @@ describe('options', () => {
"outputJsonMethods": true,
"outputPartialMethods": false,
"outputSchema": false,
"outputServices": false,
"outputServices": "default",
"outputTypeRegistry": false,
"returnObservable": false,
"snakeToCamel": true,
Expand All @@ -41,4 +41,18 @@ describe('options', () => {
addGrpcMetadata: false,
});
});

it('can set outputServices to false', () => {
const options = optionsFromParameter('outputServices=false');
expect(options).toMatchObject({
outputServices: ServiceOption.NONE,
});
});

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