diff --git a/README.markdown b/README.markdown index a24fa3d21..57b6a5933 100644 --- a/README.markdown +++ b/README.markdown @@ -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. @@ -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` versions of the message types, and the result would be both safe + succinct. @@ -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. @@ -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` @@ -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). @@ -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: diff --git a/src/generate-nestjs.ts b/src/generate-nestjs.ts index 0856f2fc5..2194b569c 100644 --- a/src/generate-nestjs.ts +++ b/src/generate-nestjs.ts @@ -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, @@ -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}>` : ''; @@ -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}>` : ``; diff --git a/src/main.ts b/src/main.ts index aaf984d92..f99a46259 100644 --- a/src/main.ts +++ b/src/main.ts @@ -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'; @@ -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)); @@ -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') { diff --git a/src/options.ts b/src/options.ts index b33c6835f..7201311ab 100644 --- a/src/options.ts +++ b/src/options.ts @@ -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; @@ -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; @@ -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, @@ -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; diff --git a/src/utils.ts b/src/utils.ts index 87b875075..5b3f8c669 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -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[] { @@ -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); } diff --git a/tests/options-test.ts b/tests/options-test.ts index 9aaf93393..80f066593 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -1,4 +1,4 @@ -import { optionsFromParameter } from '../src/options'; +import { optionsFromParameter, ServiceOption } from '../src/options'; describe('options', () => { it('can set outputJsonMethods with nestJs=true', () => { @@ -22,7 +22,7 @@ describe('options', () => { "outputJsonMethods": true, "outputPartialMethods": false, "outputSchema": false, - "outputServices": false, + "outputServices": "default", "outputTypeRegistry": false, "returnObservable": false, "snakeToCamel": true, @@ -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, + }); + }); });