Skip to content

Commit

Permalink
feat: Service generation option (#357)
Browse files Browse the repository at this point in the history
* Add ServiceOption

* Apply ServiceOption to defaults

* Add test for default options setting to NONE

* Swap to usage of ServiceOption

* Only output default services when selected

* Add additional documentation for false/none

Co-authored-by: Andrew Hewitson <ahewitson@spotify.com>
  • Loading branch information
Demiguise and Demiguise committed Sep 12, 2021
1 parent 6022582 commit 7a2cf83
Show file tree
Hide file tree
Showing 6 changed files with 50 additions and 21 deletions.
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,
});
});
});

0 comments on commit 7a2cf83

Please sign in to comment.