Skip to content

Commit

Permalink
feat: add unrecognizedEnumName and unrecognizedEnumValue options (#946)
Browse files Browse the repository at this point in the history
  • Loading branch information
vecerek committed Oct 10, 2023
1 parent 1283602 commit cd61e90
Show file tree
Hide file tree
Showing 9 changed files with 181 additions and 35 deletions.
20 changes: 14 additions & 6 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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=<NAME>` enums will contain a key `<NAME>` with value of the `unrecognizedEnumValue` option.

Defaults to `UNRECOGNIZED`.

- With `--ts_proto_opt=unrecognizedEnumValue=<NUMBER>` enums will contain a key provided by the `unrecognizedEnumName` option with value of `<NUMBER>`.

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.

Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
unrecognizedEnumName=UNKNOWN,unrecognizedEnumValue=0,stringEnums=true
Binary file not shown.
7 changes: 7 additions & 0 deletions integration/enums-with-unrecognized-name-value/test.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
syntax = "proto3";

enum StateEnum {
UNKNOWN_STATE = 0;
ON = 2;
OFF = 3;
}
51 changes: 51 additions & 0 deletions integration/enums-with-unrecognized-name-value/test.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
103 changes: 74 additions & 29 deletions src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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);

Expand All @@ -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`);
Expand All @@ -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[] = [];

Expand All @@ -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`
Expand All @@ -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[] = [];
Expand All @@ -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 {
Expand All @@ -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[] = [];
Expand All @@ -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`
Expand Down
9 changes: 9 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export type Options = {
nestJs: boolean;
env: EnvOption;
unrecognizedEnum: boolean;
unrecognizedEnumName: string;
unrecognizedEnumValue: number;
exportCommonSymbols: boolean;
outputSchema: boolean;
onlyTypes: boolean;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

Expand Down
2 changes: 2 additions & 0 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ describe("options", () => {
"stringEnums": false,
"unknownFields": false,
"unrecognizedEnum": true,
"unrecognizedEnumName": "UNRECOGNIZED",
"unrecognizedEnumValue": -1,
"useAbortSignal": false,
"useAsyncIterable": false,
"useDate": "timestamp",
Expand Down

0 comments on commit cd61e90

Please sign in to comment.