Skip to content

Commit

Permalink
feat: enumsAsLiterals option (#450)
Browse files Browse the repository at this point in the history
* feat: add `enumAsConst` option

* chore: add integration/enum-as-const

* feat: handle enumAsConst option

* chore: add integration/enum-as-const-with-string-enums

* fix: rename `enumAsConst` to `enumsAsLiterals`

* chore: update readme
  • Loading branch information
otofu-square authored Dec 24, 2021
1 parent 3103b75 commit fcaade2
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 4 deletions.
2 changes: 2 additions & 0 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,8 @@ Generated code will be placed in the Gradle build directory.

- With `--ts_proto_opt=fileSuffix=<SUFFIX>`, ts-proto will emit generated files using the specified suffix. A `helloworld.proto` file with `fileSuffix=.pb` would be generated as `helloworld.pb.ts`. This is common behavior in other protoc plugins and provides a way to quickly glob all the generated files.

- With `--ts_proto_opt=enumsAsLiterals=true`, the generated enum types will be enum-ish object with `as const`.

### Only Types

If you're looking for `ts-proto` to generate only types for your Protobuf types then passing all three of `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` as `false` is probably what you want, i.e.:
Expand Down
Binary file not shown.
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto3";

message DividerData {
enum DividerType {
DOUBLE = 0;
SINGLE = 1;
DASHED = 2;
DOTTED = 3;
}

DividerType type = 1;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* eslint-disable */
import { util, configure, Writer, Reader } from 'protobufjs/minimal';
import * as Long from 'long';

export const protobufPackage = '';

export interface DividerData {
type: DividerData_DividerType;
}

export const DividerData_DividerType = {
DOUBLE: 'DOUBLE',
SINGLE: 'SINGLE',
DASHED: 'DASHED',
DOTTED: 'DOTTED',
UNRECOGNIZED: 'UNRECOGNIZED',
} as const;

export type DividerData_DividerType = typeof DividerData_DividerType[keyof typeof DividerData_DividerType];

export function dividerData_DividerTypeFromJSON(object: any): DividerData_DividerType {
switch (object) {
case 0:
case 'DOUBLE':
return DividerData_DividerType.DOUBLE;
case 1:
case 'SINGLE':
return DividerData_DividerType.SINGLE;
case 2:
case 'DASHED':
return DividerData_DividerType.DASHED;
case 3:
case 'DOTTED':
return DividerData_DividerType.DOTTED;
case -1:
case 'UNRECOGNIZED':
default:
return DividerData_DividerType.UNRECOGNIZED;
}
}

export function dividerData_DividerTypeToJSON(object: DividerData_DividerType): string {
switch (object) {
case DividerData_DividerType.DOUBLE:
return 'DOUBLE';
case DividerData_DividerType.SINGLE:
return 'SINGLE';
case DividerData_DividerType.DASHED:
return 'DASHED';
case DividerData_DividerType.DOTTED:
return 'DOTTED';
default:
return 'UNKNOWN';
}
}

export function dividerData_DividerTypeToNumber(object: DividerData_DividerType): number {
switch (object) {
case DividerData_DividerType.DOUBLE:
return 0;
case DividerData_DividerType.SINGLE:
return 1;
case DividerData_DividerType.DASHED:
return 2;
case DividerData_DividerType.DOTTED:
return 3;
default:
return 0;
}
}

const baseDividerData: object = { type: DividerData_DividerType.DOUBLE };

export const DividerData = {
encode(message: DividerData, writer: Writer = Writer.create()): Writer {
if (message.type !== DividerData_DividerType.DOUBLE) {
writer.uint32(8).int32(dividerData_DividerTypeToNumber(message.type));
}
return writer;
},

decode(input: Reader | Uint8Array, length?: number): DividerData {
const reader = input instanceof Reader ? input : new Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseDividerData } as DividerData;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.type = dividerData_DividerTypeFromJSON(reader.int32());
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},

fromJSON(object: any): DividerData {
const message = { ...baseDividerData } as DividerData;
message.type =
object.type !== undefined && object.type !== null
? dividerData_DividerTypeFromJSON(object.type)
: DividerData_DividerType.DOUBLE;
return message;
},

toJSON(message: DividerData): unknown {
const obj: any = {};
message.type !== undefined && (obj.type = dividerData_DividerTypeToJSON(message.type));
return obj;
},

fromPartial<I extends Exact<DeepPartial<DividerData>, I>>(object: I): DividerData {
const message = { ...baseDividerData } as DividerData;
message.type = object.type ?? DividerData_DividerType.DOUBLE;
return message;
},
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;

// If you get a compile-error about 'Constructor<Long> and ... have no overlap',
// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.
if (util.Long !== Long) {
util.Long = Long as any;
configure();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
stringEnums=true,enumsAsLiterals=true
Binary file added integration/enums-as-literals/enums-as-literals.bin
Binary file not shown.
12 changes: 12 additions & 0 deletions integration/enums-as-literals/enums-as-literals.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
syntax = "proto3";

message DividerData {
enum DividerType {
DOUBLE = 0;
SINGLE = 1;
DASHED = 2;
DOTTED = 3;
}

DividerType type = 1;
}
126 changes: 126 additions & 0 deletions integration/enums-as-literals/enums-as-literals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/* eslint-disable */
import { util, configure, Writer, Reader } from 'protobufjs/minimal';
import * as Long from 'long';

export const protobufPackage = '';

export interface DividerData {
type: DividerData_DividerType;
}

export const DividerData_DividerType = {
DOUBLE: 0,
SINGLE: 1,
DASHED: 2,
DOTTED: 3,
UNRECOGNIZED: -1,
} as const;

export type DividerData_DividerType = typeof DividerData_DividerType[keyof typeof DividerData_DividerType];

export function dividerData_DividerTypeFromJSON(object: any): DividerData_DividerType {
switch (object) {
case 0:
case 'DOUBLE':
return DividerData_DividerType.DOUBLE;
case 1:
case 'SINGLE':
return DividerData_DividerType.SINGLE;
case 2:
case 'DASHED':
return DividerData_DividerType.DASHED;
case 3:
case 'DOTTED':
return DividerData_DividerType.DOTTED;
case -1:
case 'UNRECOGNIZED':
default:
return DividerData_DividerType.UNRECOGNIZED;
}
}

export function dividerData_DividerTypeToJSON(object: DividerData_DividerType): string {
switch (object) {
case DividerData_DividerType.DOUBLE:
return 'DOUBLE';
case DividerData_DividerType.SINGLE:
return 'SINGLE';
case DividerData_DividerType.DASHED:
return 'DASHED';
case DividerData_DividerType.DOTTED:
return 'DOTTED';
default:
return 'UNKNOWN';
}
}

const baseDividerData: object = { type: 0 };

export const DividerData = {
encode(message: DividerData, writer: Writer = Writer.create()): Writer {
if (message.type !== 0) {
writer.uint32(8).int32(message.type);
}
return writer;
},

decode(input: Reader | Uint8Array, length?: number): DividerData {
const reader = input instanceof Reader ? input : new Reader(input);
let end = length === undefined ? reader.len : reader.pos + length;
const message = { ...baseDividerData } as DividerData;
while (reader.pos < end) {
const tag = reader.uint32();
switch (tag >>> 3) {
case 1:
message.type = reader.int32() as any;
break;
default:
reader.skipType(tag & 7);
break;
}
}
return message;
},

fromJSON(object: any): DividerData {
const message = { ...baseDividerData } as DividerData;
message.type = object.type !== undefined && object.type !== null ? dividerData_DividerTypeFromJSON(object.type) : 0;
return message;
},

toJSON(message: DividerData): unknown {
const obj: any = {};
message.type !== undefined && (obj.type = dividerData_DividerTypeToJSON(message.type));
return obj;
},

fromPartial<I extends Exact<DeepPartial<DividerData>, I>>(object: I): DividerData {
const message = { ...baseDividerData } as DividerData;
message.type = object.type ?? 0;
return message;
},
};

type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined;

export type DeepPartial<T> = T extends Builtin
? T
: T extends Array<infer U>
? Array<DeepPartial<U>>
: T extends ReadonlyArray<infer U>
? ReadonlyArray<DeepPartial<U>>
: T extends {}
? { [K in keyof T]?: DeepPartial<T[K]> }
: Partial<T>;

type KeysOfUnion<T> = T extends T ? keyof T : never;
export type Exact<P, I extends P> = P extends Builtin
? P
: P & { [K in keyof P]: Exact<P[K], I[K]> } & Record<Exclude<keyof I, KeysOfUnion<P>>, never>;

// If you get a compile-error about 'Constructor<Long> and ... have no overlap',
// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'.
if (util.Long !== Long) {
util.Long = Long as any;
configure();
}
1 change: 1 addition & 0 deletions integration/enums-as-literals/parameters.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
enumsAsLiterals=true
22 changes: 18 additions & 4 deletions src/enums.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,36 @@ export function generateEnum(
const chunks: Code[] = [];

maybeAddComment(sourceInfo, chunks, enumDesc.options?.deprecated);
chunks.push(code`export ${options.constEnums ? 'const ' : ''}enum ${def(fullName)} {`);

if (options.enumsAsLiterals) {
chunks.push(code`export const ${def(fullName)} = {`);
} else {
chunks.push(code`export ${options.constEnums ? 'const ' : ''}enum ${def(fullName)} {`);
}

const delimiter = options.enumsAsLiterals ? ':' : '=';

enumDesc.value.forEach((valueDesc, index) => {
const info = sourceInfo.lookup(Fields.enum.value, index);
maybeAddComment(info, chunks, valueDesc.options?.deprecated, `${valueDesc.name} - `);
chunks.push(
code`${valueDesc.name} = ${options.stringEnums ? `"${valueDesc.name}"` : valueDesc.number.toString()},`
code`${valueDesc.name} ${delimiter} ${options.stringEnums ? `"${valueDesc.name}"` : valueDesc.number.toString()},`
);
});

if (options.unrecognizedEnum)
chunks.push(code`
${UNRECOGNIZED_ENUM_NAME} = ${
${UNRECOGNIZED_ENUM_NAME} ${delimiter} ${
options.stringEnums ? `"${UNRECOGNIZED_ENUM_NAME}"` : UNRECOGNIZED_ENUM_VALUE.toString()
},`);
chunks.push(code`}`);

if (options.enumsAsLiterals) {
chunks.push(code`} as const`);
chunks.push(code`\n`);
chunks.push(code`export type ${def(fullName)} = typeof ${def(fullName)}[keyof typeof ${def(fullName)}]`);
} else {
chunks.push(code`}`);
}

if (options.outputJsonMethods || (options.stringEnums && options.outputEncodeMethods)) {
chunks.push(code`\n`);
Expand Down
2 changes: 2 additions & 0 deletions src/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export type Options = {
outputTypeRegistry: boolean;
stringEnums: boolean;
constEnums: boolean;
enumsAsLiterals: boolean;
outputClientImpl: boolean | 'grpc-web';
outputServices: ServiceOption;
addGrpcMetadata: boolean;
Expand Down Expand Up @@ -76,6 +77,7 @@ export function defaultOptions(): Options {
outputTypeRegistry: false,
stringEnums: false,
constEnums: false,
enumsAsLiterals: false,
outputClientImpl: true,
outputServices: ServiceOption.DEFAULT,
returnObservable: false,
Expand Down
1 change: 1 addition & 0 deletions tests/options-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ describe('options', () => {
"constEnums": false,
"context": false,
"emitImportedFiles": true,
"enumsAsLiterals": false,
"env": "both",
"esModuleInterop": false,
"exportCommonSymbols": true,
Expand Down

0 comments on commit fcaade2

Please sign in to comment.