Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for custom options on: oneofs, enums, and enum values #562

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 68 additions & 2 deletions MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,8 +1030,8 @@ operations.

## Custom options

`protobuf-ts` supports custom options for messages, fields, services
and methods and will add them to the reflection information.
`protobuf-ts` supports custom options for messages, fields, oneofs, enums, enum
values, services and methods and will add them to the reflection information.

For example, consider the following service definition in
[service-annotated.proto](./packages/proto/service-annotated.proto):
Expand Down Expand Up @@ -1105,12 +1105,78 @@ import {JsonValue} from "@protobuf-ts/runtime";
let rule: JsonValue | undefined = readMethodOption(AnnotatedService, "get", "google.api.http");
```

Because `protobuf-ts` uses TypeScript enums to represent protobuf enums the
custom options for enums and enum options must be stored in a separate export.
For example a protobuf enum like this:

```proto
enum MyEnum {
option allow_alias = true;
option (enum_opt) = -789;
MY_ENUM_ANY = 0 [(enum_value_opt) = 0];
MY_ENUM_YES = 1 [(enum_value_opt) = 123];
MY_ENUM_TRUE = 1 [(enum_value_opt) = 456];
MY_ENUM_NO = 2;
MY_ENUM_FALSE = 2;
}
```

Will generate two exports: `MyEnum` (the TypeScript enum) and `MyEnumInfo` (the
reflection information about the enum). Note that the latter is the same value
returned for `MyMessage.field[0].T()` (assuming that field is an enum):

```ts
export enum MyEnum {
/**
* @generated from protobuf enum value: ANY = 0;
*/
ANY = 0,
/**
* @generated from protobuf enum value: YES = 1;
*/
YES = 1,
/**
* @generated from protobuf enum value: TRUE = 1;
*/
TRUE = 1,
/**
* @generated from protobuf enum value: NO = 2;
*/
NO = 2,
/**
* @generated from protobuf enum value: FALSE = 2;
*/
FALSE = 2,
}

export const MyEnumInfo: EnumInfo = [
"package.MyEnum",
MyEnum,
"MY_ENUM_",
{allowAlias: true, options: {enum_opt: -789}, valueOptions: {ANY: {enum_value_opt: 0}, YES: {enum_value_opt: 123}, TRUE: {enum_value_opt: 456}}}
];
```

Accessing the custom options for enums or enum options still uses the the
actual TypeScript enum, not the enum info object:

```ts
let enumOption = readEnumOption(MyEnum, "enum_opt"); // -789
let enumAnyOption = readEnumValueOption(MyEnum, MyEnum.ANY, "enum_value_opt"); // 0

// Use string literal for aliased enum values
let enumYesOption = readEnumValueOption(MyEnum, "YES", "enum_value_opt"); // 123
let enumTrueOption = readEnumValueOption(MyEnum, "TRUE", "enum_value_opt"); // 456
```


| Options for | stored in | access with |
|-------------|---------------------------------------|-----------------------------------------------------|
| Messages | `AnnotatedMessage.options` | `readMessageOption()` from @protobuf-ts/runtime |
| Oneofs | `AnnotatedMessage.oneofOptions[name]` | `readOneofOption()` from @protobuf-ts/runtime |
| Fields | `AnnotatedMessage.field[0].options` | `readFieldOption()` from @protobuf-ts/runtime |
| Enums | `AnnotatedEnumInfo.options` | `readEnumOption()` from @protobuf-ts/runtime |
| Enum Values | `AnnotatedEnumInfo.valueOptions` | `readEnumValueOption()` from @protobuf-ts/runtime |
| Services | `AnnotatedService.options` | `readServiceOption()` from @protobuf-ts/runtime-rpc |
| Methods | `AnnotatedService.methods[0].options` | `readMethodOption()` from @protobuf-ts/runtime-rpc |

Expand Down
80 changes: 79 additions & 1 deletion packages/plugin/src/code-gen/enum-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import {
SymbolTable,
TypescriptEnumBuilder,
TypescriptFile,
TypeScriptImports
TypeScriptImports,
typescriptLiteralFromValue
} from "@protobuf-ts/plugin-framework";
import {CommentGenerator} from "./comment-generator";
import {Interpreter} from "../interpreter";
Expand All @@ -18,6 +19,7 @@ export class EnumGenerator extends GeneratorBase {

constructor(symbols: SymbolTable, registry: DescriptorRegistry, imports: TypeScriptImports, comments: CommentGenerator, interpreter: Interpreter,
private readonly options: {
runtimeImportPath: string;
}) {
super(symbols, registry, imports, comments, interpreter);
}
Expand Down Expand Up @@ -79,9 +81,85 @@ export class EnumGenerator extends GeneratorBase {
);
// add to our file
source.addStatement(statement);
this.generateEnumInfo(source, descriptor);
this.comments.addCommentsForDescriptor(statement, descriptor, 'appendToLeadingBlock');
return statement;
}

/**
* For the following .proto:
*
* ```proto
* enum MyEnum {
* option (enum_opt1) = 1003;
*
* MY_ENUM_FOO = 0 [(enum_value_opt1) = 1004];
* MY_ENUM_BAR = 1;
* }
* ```
*
* We generate the following enum info:
* ```typescript
* export const MyEnumInfo: EnumInfo = [
* 'package.MyEnum',
* MyEnum,
* 'MY_ENUM_',
* registerEnumOptions(MyEnum, {options: {enum_opt1: 1003}, valueOptions: {FOO: {enum_value_opt1: 1004}}})
* ];
* ```
*/
generateEnumInfo(source: TypescriptFile, descriptor: EnumDescriptorProto): void {
let enumInfo = this.interpreter.getEnumInfo(descriptor),
[pbTypeName, , sharedPrefix, options] = enumInfo,
EnumInfoType = this.imports.name(source, 'EnumInfo', this.options.runtimeImportPath, true),
generatedEnum = this.imports.type(source, descriptor),
generatedEnumInfo = this.symbols.register(`${generatedEnum}Info`, descriptor, source, 'info'),
enumInfoLiteral: ts.Expression[] = [
// 'package.MyEnum'
ts.createStringLiteral(pbTypeName),
// MyEnum,
ts.createIdentifier(generatedEnum),
];

if (sharedPrefix || options) {
// 'MY_ENUM_'
enumInfoLiteral[2] = typescriptLiteralFromValue(sharedPrefix);
if (options) {
// registerEnumOptions(MyEnum, {options: {enum_opt1: 1003}, valueOptions: {FOO: {enum_value_opt1: 1004}}})
enumInfoLiteral[3] = ts.createCall(
ts.createIdentifier(
this.imports.name(source, 'registerEnumOptions', this.options.runtimeImportPath)
),
undefined,
[ts.createIdentifier(generatedEnum), typescriptLiteralFromValue({...options})]
)
}
}

// export const MyEnumInfo: EnumInfo = [
// 'package.MyEnum',
// MyEnum,
// 'MY_ENUM_',
// registerEnumOptions(MyEnum, {options: {enum_opt1: 1003}, valueOptions: {FOO: {enum_value_opt1: 1004}}})
// ];
const exportConst = ts.createVariableStatement(
[ts.createModifier(ts.SyntaxKind.ExportKeyword)],
ts.createVariableDeclarationList(
[ts.createVariableDeclaration(
generatedEnumInfo,
ts.createTypeReferenceNode(
ts.createIdentifier(EnumInfoType),
undefined
),
ts.createArrayLiteral(enumInfoLiteral, true)
)],
ts.NodeFlags.Const
)
);

// add to our file
source.addStatement(exportConst);
}


}
16 changes: 4 additions & 12 deletions packages/plugin/src/code-gen/field-info-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,21 +147,13 @@ export class FieldInfoGenerator {
);
}

private createEnumT(source: TypescriptFile, ei: rt.EnumInfo): ts.ArrowFunction {
let [pbTypeName, , sharedPrefix] = ei,
descriptor = this.registry.resolveTypeName(pbTypeName),
generatedEnum = this.imports.type(source, descriptor),
enumInfoLiteral: ts.Expression[] = [
ts.createStringLiteral(pbTypeName),
ts.createIdentifier(generatedEnum),
];
if (sharedPrefix) {
enumInfoLiteral.push(ts.createStringLiteral(sharedPrefix));
}
private createEnumT(source: TypescriptFile, [typeName]: rt.EnumInfo): ts.ArrowFunction {
let descriptor = this.registry.resolveTypeName(typeName),
generatedEnumInfo = this.imports.type(source, descriptor, 'info');
return ts.createArrowFunction(
undefined, undefined, [], undefined,
ts.createToken(ts.SyntaxKind.EqualsGreaterThanToken),
ts.createArrayLiteral(enumInfoLiteral, false)
ts.createIdentifier(generatedEnumInfo)
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,7 @@ export class MessageInterfaceGenerator extends GeneratorBase {
let [enumTypeName] = ei;
let enumDescriptor = this.registry.resolveTypeName(enumTypeName);
assert(EnumDescriptorProto.is(enumDescriptor));
return ts.createTypeReferenceNode(this.imports.type(source, enumDescriptor), undefined);
return ts.createTypeReferenceNode(this.imports.type(source, enumDescriptor, undefined, true), undefined);
}


Expand Down
16 changes: 14 additions & 2 deletions packages/plugin/src/code-gen/message-type-generator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,11 +92,23 @@ export class MessageTypeGenerator extends GeneratorBase {
this.fieldInfoGenerator.createFieldInfoLiterals(source, interpreterType.fields)
];

// if present, add message options in json format to MessageType CTOR args
if (Object.keys(interpreterType.options).length) {
const hasMessageOptions = Object.keys(interpreterType.options).length;
const hasOneofOptions = Object.keys(interpreterType.oneofOptions).length;
// if present, add message/oneof options in json format to MessageType CTOR args
if (hasMessageOptions) {
classDecSuperArgs.push(
typescriptLiteralFromValue(interpreterType.options)
);
if (hasOneofOptions) {
classDecSuperArgs.push(
typescriptLiteralFromValue(interpreterType.oneofOptions)
);
}
} else if (hasOneofOptions) {
classDecSuperArgs.push(
typescriptLiteralFromValue({}),
typescriptLiteralFromValue(interpreterType.oneofOptions)
);
}

// "MyMessage$Type" constructor() { super(...) }
Expand Down
Loading