diff --git a/README.markdown b/README.markdown index 9c7f1fcaa..77e2ddf10 100644 --- a/README.markdown +++ b/README.markdown @@ -14,15 +14,15 @@ - [Buf](#buf) - [ESM](#esm) - [Goals](#goals) + - [Non-Goals](#non-goals) - [Example Types](#example-types) - [Highlights](#highlights) - [Auto-Batching / N+1 Prevention](#auto-batching--n1-prevention) - [Usage](#usage) - - [Supported options](#supported-options) - - [Only Types](#only-types) - - [NestJS Support](#nestjs-support) - - [Watch Mode](#watch-mode) - - [Basic gRPC implementation](#basic-grpc-implementation) + - [Supported options](#supported-options) + - [NestJS Support](#nestjs-support) + - [Watch Mode](#watch-mode) + - [Basic gRPC implementation](#basic-grpc-implementation) - [Sponsors](#sponsors) - [Development](#development) - [Assumptions](#assumptions) @@ -354,7 +354,15 @@ Generated code will be placed in the Gradle build directory. See the "OneOf Handling" section. -- With `--ts_proto_opt=unrecognizedEnum=false` enums will not contain an `UNRECOGNIZED` key with value of -1. +- With `--ts_proto_opt=unrecognizedEnumName=` enums will contain a key `` with value of the `unrecognizedEnumValue` option. + + Defaults to `UNRECOGNIZED`. + +- With `--ts_proto_opt=unrecognizedEnumValue=` enums will contain a key provided by the `unrecognizedEnumName` option with value of ``. + + Defaults to `-1`. + +- With `--ts_proto_opt=unrecognizedEnum=false` enums will not contain an unrecognized enum key and value as provided by the `unrecognizedEnumName` and `unrecognizedEnumValue` options. - With `--ts_proto_opt=removeEnumPrefix=true` generated enums will have the enum name removed from members. diff --git a/integration/enums-with-unrecognized-name-value/default-value-test.ts b/integration/enums-with-unrecognized-name-value/default-value-test.ts new file mode 100644 index 000000000..b515f835e --- /dev/null +++ b/integration/enums-with-unrecognized-name-value/default-value-test.ts @@ -0,0 +1,23 @@ +import { stateEnumFromJSON, stateEnumToJSON, stateEnumToNumber, StateEnum } from "./test"; + +describe("enums-with-unrecognized-name-value", () => { + describe("stateEnumFromJSON", () => { + it("returns correct default state", () => { + expect(stateEnumFromJSON("non-existent")).toBe(StateEnum.UNKNOWN_STATE); + }); + }); + + describe("stateEnumToJSON", () => { + it("returns correct default state", () => { + // @ts-expect-error Argument of type '1' is not assignable to parameter of type 'StateEnum'. + expect(stateEnumToJSON(1)).toBe("UNKNOWN_STATE"); + }); + }); + + describe("stateEnumToNumber", () => { + it("returns correct default state", () => { + // @ts-expect-error Argument of type '1' is not assignable to parameter of type 'StateEnum'. + expect(stateEnumToNumber(1)).toBe(0); + }); + }); +}); diff --git a/integration/enums-with-unrecognized-name-value/parameters.txt b/integration/enums-with-unrecognized-name-value/parameters.txt new file mode 100644 index 000000000..67b950f46 --- /dev/null +++ b/integration/enums-with-unrecognized-name-value/parameters.txt @@ -0,0 +1 @@ +unrecognizedEnumName=UNKNOWN,unrecognizedEnumValue=0,stringEnums=true diff --git a/integration/enums-with-unrecognized-name-value/test.bin b/integration/enums-with-unrecognized-name-value/test.bin new file mode 100644 index 000000000..417ca199c Binary files /dev/null and b/integration/enums-with-unrecognized-name-value/test.bin differ diff --git a/integration/enums-with-unrecognized-name-value/test.proto b/integration/enums-with-unrecognized-name-value/test.proto new file mode 100644 index 000000000..51427b352 --- /dev/null +++ b/integration/enums-with-unrecognized-name-value/test.proto @@ -0,0 +1,7 @@ +syntax = "proto3"; + +enum StateEnum { + UNKNOWN_STATE = 0; + ON = 2; + OFF = 3; +} diff --git a/integration/enums-with-unrecognized-name-value/test.ts b/integration/enums-with-unrecognized-name-value/test.ts new file mode 100644 index 000000000..71b2415ef --- /dev/null +++ b/integration/enums-with-unrecognized-name-value/test.ts @@ -0,0 +1,51 @@ +/* eslint-disable */ + +export const protobufPackage = ""; + +export enum StateEnum { + UNKNOWN_STATE = "UNKNOWN_STATE", + ON = "ON", + OFF = "OFF", +} + +export function stateEnumFromJSON(object: any): StateEnum { + switch (object) { + case 0: + case "UNKNOWN_STATE": + return StateEnum.UNKNOWN_STATE; + case 2: + case "ON": + return StateEnum.ON; + case 3: + case "OFF": + return StateEnum.OFF; + default: + return StateEnum.UNKNOWN_STATE; + } +} + +export function stateEnumToJSON(object: StateEnum): string { + switch (object) { + case StateEnum.UNKNOWN_STATE: + return "UNKNOWN_STATE"; + case StateEnum.ON: + return "ON"; + case StateEnum.OFF: + return "OFF"; + default: + return "UNKNOWN_STATE"; + } +} + +export function stateEnumToNumber(object: StateEnum): number { + switch (object) { + case StateEnum.UNKNOWN_STATE: + return 0; + case StateEnum.ON: + return 2; + case StateEnum.OFF: + return 3; + default: + return 0; + } +} diff --git a/src/enums.ts b/src/enums.ts index 042ded819..aa099f4d0 100644 --- a/src/enums.ts +++ b/src/enums.ts @@ -5,8 +5,7 @@ import { uncapitalize, camelToSnake } from "./case"; import SourceInfo, { Fields } from "./sourceInfo"; import { Context } from "./context"; -const UNRECOGNIZED_ENUM_NAME = "UNRECOGNIZED"; -const UNRECOGNIZED_ENUM_VALUE = -1; +type UnrecognizedEnum = { present: false } | { present: true; name: string }; // Output the `enum { Foo, A = 0, B = 1 }` export function generateEnum( @@ -17,6 +16,7 @@ export function generateEnum( ): Code { const { options } = ctx; const chunks: Code[] = []; + let unrecognizedEnum: UnrecognizedEnum = { present: false }; maybeAddComment(sourceInfo, chunks, enumDesc.options?.deprecated); @@ -32,17 +32,21 @@ export function generateEnum( const info = sourceInfo.lookup(Fields.enum.value, index); const valueName = getValueName(ctx, fullName, valueDesc); const memberName = getMemberName(ctx, enumDesc, valueDesc); + if (valueDesc.number === options.unrecognizedEnumValue) { + unrecognizedEnum = { present: true, name: memberName }; + } maybeAddComment(info, chunks, valueDesc.options?.deprecated, `${memberName} - `); chunks.push( code`${memberName} ${delimiter} ${options.stringEnums ? `"${valueName}"` : valueDesc.number.toString()},`, ); }); - if (options.unrecognizedEnum) + if (options.unrecognizedEnum && !unrecognizedEnum.present) { chunks.push(code` - ${UNRECOGNIZED_ENUM_NAME} ${delimiter} ${ - options.stringEnums ? `"${UNRECOGNIZED_ENUM_NAME}"` : UNRECOGNIZED_ENUM_VALUE.toString() + ${options.unrecognizedEnumName} ${delimiter} ${ + options.stringEnums ? `"${options.unrecognizedEnumName}"` : options.unrecognizedEnumValue.toString() },`); + } if (options.enumsAsLiterals) { chunks.push(code`} as const`); @@ -58,22 +62,27 @@ export function generateEnum( (options.stringEnums && options.outputEncodeMethods) ) { chunks.push(code`\n`); - chunks.push(generateEnumFromJson(ctx, fullName, enumDesc)); + chunks.push(generateEnumFromJson(ctx, fullName, enumDesc, unrecognizedEnum)); } if (options.outputJsonMethods === true || options.outputJsonMethods === "to-only") { chunks.push(code`\n`); - chunks.push(generateEnumToJson(ctx, fullName, enumDesc)); + chunks.push(generateEnumToJson(ctx, fullName, enumDesc, unrecognizedEnum)); } if (options.stringEnums && options.outputEncodeMethods) { chunks.push(code`\n`); - chunks.push(generateEnumToNumber(ctx, fullName, enumDesc)); + chunks.push(generateEnumToNumber(ctx, fullName, enumDesc, unrecognizedEnum)); } return joinCode(chunks, { on: "\n" }); } /** Generates a function with a big switch statement to decode JSON -> our enum. */ -export function generateEnumFromJson(ctx: Context, fullName: string, enumDesc: EnumDescriptorProto): Code { +export function generateEnumFromJson( + ctx: Context, + fullName: string, + enumDesc: EnumDescriptorProto, + unrecognizedEnum: UnrecognizedEnum, +): Code { const { options, utils } = ctx; const chunks: Code[] = []; @@ -92,12 +101,19 @@ export function generateEnumFromJson(ctx: Context, fullName: string, enumDesc: E } if (options.unrecognizedEnum) { - chunks.push(code` - case ${UNRECOGNIZED_ENUM_VALUE}: - case "${UNRECOGNIZED_ENUM_NAME}": - default: - return ${fullName}.${UNRECOGNIZED_ENUM_NAME}; - `); + if (!unrecognizedEnum.present) { + chunks.push(code` + case ${options.unrecognizedEnumValue}: + case "${options.unrecognizedEnumName}": + default: + return ${fullName}.${options.unrecognizedEnumName}; + `); + } else { + chunks.push(code` + default: + return ${fullName}.${unrecognizedEnum.name}; + `); + } } else { // We use globalThis to avoid conflicts on protobuf types named `Error`. chunks.push(code` @@ -112,7 +128,12 @@ export function generateEnumFromJson(ctx: Context, fullName: string, enumDesc: E } /** Generates a function with a big switch statement to encode our enum -> JSON. */ -export function generateEnumToJson(ctx: Context, fullName: string, enumDesc: EnumDescriptorProto): Code { +export function generateEnumToJson( + ctx: Context, + fullName: string, + enumDesc: EnumDescriptorProto, + unrecognizedEnum: UnrecognizedEnum, +): Code { const { options, utils } = ctx; const chunks: Code[] = []; @@ -137,18 +158,30 @@ export function generateEnumToJson(ctx: Context, fullName: string, enumDesc: Enu } if (options.unrecognizedEnum) { - chunks.push(code` - case ${fullName}.${UNRECOGNIZED_ENUM_NAME}:`); + if (!unrecognizedEnum.present) { + chunks.push(code` + case ${fullName}.${options.unrecognizedEnumName}:`); - if (ctx.options.useNumericEnumForJson) { + if (ctx.options.useNumericEnumForJson) { + chunks.push(code` + default: + return ${options.unrecognizedEnumValue}; + `); + } else { + chunks.push(code` + default: + return "${options.unrecognizedEnumName}"; + `); + } + } else if (ctx.options.useNumericEnumForJson) { chunks.push(code` - default: - return ${UNRECOGNIZED_ENUM_VALUE}; - `); + default: + return ${options.unrecognizedEnumValue}; + `); } else { chunks.push(code` default: - return "${UNRECOGNIZED_ENUM_NAME}"; + return "${unrecognizedEnum.name}"; `); } } else { @@ -165,7 +198,12 @@ export function generateEnumToJson(ctx: Context, fullName: string, enumDesc: Enu } /** Generates a function with a big switch statement to encode our string enum -> int value. */ -export function generateEnumToNumber(ctx: Context, fullName: string, enumDesc: EnumDescriptorProto): Code { +export function generateEnumToNumber( + ctx: Context, + fullName: string, + enumDesc: EnumDescriptorProto, + unrecognizedEnum: UnrecognizedEnum, +): Code { const { options, utils } = ctx; const chunks: Code[] = []; @@ -178,11 +216,18 @@ export function generateEnumToNumber(ctx: Context, fullName: string, enumDesc: E } if (options.unrecognizedEnum) { - chunks.push(code` - case ${fullName}.${UNRECOGNIZED_ENUM_NAME}: - default: - return ${UNRECOGNIZED_ENUM_VALUE}; - `); + if (!unrecognizedEnum.present) { + chunks.push(code` + case ${fullName}.${options.unrecognizedEnumName}: + default: + return ${options.unrecognizedEnumValue}; + `); + } else { + chunks.push(code` + default: + return ${options.unrecognizedEnumValue}; + `); + } } else { // We use globalThis to avoid conflicts on protobuf types named `Error`. chunks.push(code` diff --git a/src/options.ts b/src/options.ts index 8fdda8e1c..b7d4d8fe4 100644 --- a/src/options.ts +++ b/src/options.ts @@ -66,6 +66,8 @@ export type Options = { nestJs: boolean; env: EnvOption; unrecognizedEnum: boolean; + unrecognizedEnumName: string; + unrecognizedEnumValue: number; exportCommonSymbols: boolean; outputSchema: boolean; onlyTypes: boolean; @@ -119,6 +121,8 @@ export function defaultOptions(): Options { nestJs: false, env: EnvOption.BOTH, unrecognizedEnum: true, + unrecognizedEnumName: "UNRECOGNIZED", + unrecognizedEnumValue: -1, exportCommonSymbols: true, outputSchema: false, onlyTypes: false, @@ -234,6 +238,11 @@ export function optionsFromParameter(parameter: string | undefined): Options { options.exportCommonSymbols = false; } + if (options.unrecognizedEnumValue) { + // Make sure to cast number options to an actual number + options.unrecognizedEnumValue = Number(options.unrecognizedEnumValue); + } + return options; } diff --git a/tests/options-test.ts b/tests/options-test.ts index 9a45bbef9..bce760654 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -46,6 +46,8 @@ describe("options", () => { "stringEnums": false, "unknownFields": false, "unrecognizedEnum": true, + "unrecognizedEnumName": "UNRECOGNIZED", + "unrecognizedEnumValue": -1, "useAbortSignal": false, "useAsyncIterable": false, "useDate": "timestamp",