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 oneof custom options #553

Closed
wants to merge 1 commit into from
Closed
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
3 changes: 2 additions & 1 deletion MANUAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -1030,7 +1030,7 @@ operations.

## Custom options

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

For example, consider the following service definition in
Expand Down Expand Up @@ -1110,6 +1110,7 @@ let rule: JsonValue | undefined = readMethodOption(AnnotatedService, "get", "goo
| 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 |
| Services | `AnnotatedService.options` | `readServiceOption()` from @protobuf-ts/runtime-rpc |
| Methods | `AnnotatedService.methods[0].options` | `readMethodOption()` from @protobuf-ts/runtime-rpc |
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
51 changes: 38 additions & 13 deletions packages/plugin/src/interpreter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import {
AnyDescriptorProto,
DescriptorProto,
DescriptorRegistry,
EnumDescriptorProto,
EnumValueDescriptorProto,
FieldDescriptorProto,
FieldOptions_JSType,
FileDescriptorProto,
Expand All @@ -17,11 +19,6 @@ import { FieldInfoGenerator } from "./code-gen/field-info-generator";
import {OurFileOptions, OurServiceOptions, readOurFileOptions, readOurServiceOptions} from "./our-options";


type JsonOptionsMap = {
[extensionName: string]: rt.JsonValue
}


/**
* The protobuf-ts plugin generates code for message types from descriptor
* protos. This class also creates message types from descriptor protos, but
Expand Down Expand Up @@ -86,7 +83,19 @@ export class Interpreter {
* Note that options on options (google.protobuf.*Options) are not
* supported.
*/
readOptions(descriptor: FieldDescriptorProto | MethodDescriptorProto | FileDescriptorProto | ServiceDescriptorProto | DescriptorProto, excludeOptions: readonly string[]): JsonOptionsMap | undefined {
readOptions(descriptor: FileDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'FileOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: DescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'MessageOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: FieldDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'FieldOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: OneofDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'OneofOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: EnumDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'EnumOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: EnumValueDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'EnumValueOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: ServiceDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'ServiceOptions'): rt.JsonOptionsMap | undefined
readOptions(descriptor: MethodDescriptorProto, excludeOptions: readonly string[], knownOptionTypeName: 'MethodOptions'): rt.JsonOptionsMap | undefined
readOptions(
descriptor: AnyDescriptorProto,
excludeOptions: readonly string[],
knownOptionTypeName?: 'FileOptions' | 'MessageOptions' | 'FieldOptions' | 'OneofOptions' | 'EnumOptions' | 'EnumValueOptions' | 'ServiceOptions' | 'MethodOptions'
): rt.JsonOptionsMap | undefined {

// the option to force exclude all options takes precedence
if (this.options.forceExcludeAllOptions) {
Expand All @@ -105,7 +114,11 @@ export class Interpreter {
}

let optionsTypeName: string;
if (FieldDescriptorProto.is(descriptor) && DescriptorProto.is(this.registry.parentOf(descriptor))) {
if (knownOptionTypeName) {
optionsTypeName = `google.protobuf.${knownOptionTypeName}`;
} else if (OneofDescriptorProto.is(descriptor) && DescriptorProto.is(this.registry.parentOf(descriptor))) {
optionsTypeName = 'google.protobuf.OneofOptions';
} else if (FieldDescriptorProto.is(descriptor) && DescriptorProto.is(this.registry.parentOf(descriptor))) {
Comment on lines +119 to +121
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately the way MessageType#is behaves (assignable and no excess properties) means that the order of these if statements is extremely important. I spent an hour trying to figure out why the oneof options weren't being populated when I put the OneofDescriptorProto.is(descriptor) check after the FieldDescriptorProto.is(descriptor) check. Well, it's because FieldDescriptorProto.is(oneofDescriptor) && DescriptorProto.is(this.registry.parentOf(oneofDescriptor)) returns true.

One way to work around this is what I've done in this PR is to have the caller just tell us which option type name goes with the descriptor. But I've also realized that we can just use the MESSAGE_TYPE symbol on the descriptor to know which descriptor MessageType it is and therefore which option type name should be used so I'm going to switch to using that.

optionsTypeName = 'google.protobuf.FieldOptions';
} else if (MethodDescriptorProto.is(descriptor)) {
optionsTypeName = 'google.protobuf.MethodOptions';
Expand Down Expand Up @@ -201,29 +214,41 @@ export class Interpreter {
if (!type) {

// Create and store the message type
const optionsPlaceholder: JsonOptionsMap = {};
const oneofOptions: {[oneof: string]: rt.JsonOptionsMap} = {};
const optionsPlaceholder: rt.JsonOptionsMap = {};
type = new rt.MessageType(
typeName,
this.buildFieldInfos(descriptor.field),
optionsPlaceholder
optionsPlaceholder,
oneofOptions
);
this.messageTypes.set(typeName, type);

const ourFileOptions = this.readOurFileOptions(this.registry.fileOf(descriptor));
const excludeOptions = ourFileOptions["ts.exclude_options"];

// add message options *after* storing, so that the option can refer to itself
const messageOptions = this.readOptions(descriptor, ourFileOptions["ts.exclude_options"]);
const messageOptions = this.readOptions(descriptor, excludeOptions, 'MessageOptions');
if (messageOptions) {
for (let key of Object.keys(messageOptions)) {
optionsPlaceholder[key] = messageOptions[key];
}
}

// same for oneof options
for (let i = 0; i < descriptor.oneofDecl.length; i++) {
const od = descriptor.oneofDecl[i];
const oneofOpt = this.readOptions(od, excludeOptions, 'OneofOptions');
if (oneofOpt) {
oneofOptions[this.createTypescriptNameForField(od)] = oneofOpt;
}
}

// same for field options
for (let i = 0; i < type.fields.length; i++) {
const fd = descriptor.field[i];
const fi = type.fields[i];
fi.options = this.readOptions(fd, ourFileOptions["ts.exclude_options"]);
fi.options = this.readOptions(fd, excludeOptions, 'FieldOptions');
}

}
Expand Down Expand Up @@ -295,7 +320,7 @@ export class Interpreter {
return new rpc.ServiceType(
typeName,
methods.map(m => this.buildMethodInfo(m, excludeOptions)),
this.readOptions(desc, excludeOptions)
this.readOptions(desc, excludeOptions, 'ServiceOptions')
);
}

Expand Down Expand Up @@ -340,7 +365,7 @@ export class Interpreter {
info.O = this.getMessageType(methodDescriptor.outputType);

// options: Contains custom method options from the .proto source in JSON format.
info.options = this.readOptions(methodDescriptor, excludeOptions);
info.options = this.readOptions(methodDescriptor, excludeOptions, 'MethodOptions');

return info as rpc.PartialMethodInfo;
}
Expand Down
11 changes: 11 additions & 0 deletions packages/proto/msg-annotated.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,10 @@ extend google.protobuf.MessageOptions {

}

extend google.protobuf.OneofOptions {
int32 opt_oneof = 1501;
}


// Used for options
enum OptionEnum {
Expand Down Expand Up @@ -91,5 +95,12 @@ message AnnotatedMessage {
// annotated with a field option defined right in the message
int32 ann_local = 10 [(spec.AnnotatedMessage.local_opt) = true];

oneof ann_oneof {

option (spec.opt_oneof) = -99;

bool example = 11;
}

}

1 change: 1 addition & 0 deletions packages/runtime/spec/reflection-merge-partial.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ describe('reflectionMergePartial()', () => {
normalizeFieldInfo({kind: "message", T: () => childHandler, no: 1, name: 'child'}),
normalizeFieldInfo({kind: "message", T: () => childHandler, no: 2, name: 'children', repeat: RepeatType.UNPACKED}),
],
oneofOptions: {},
options: {}
};
});
Expand Down
1 change: 1 addition & 0 deletions packages/runtime/spec/support/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ export function stubMessageType(name = '.test.Message'): IMessageType<any> {
typeName: name,
fields: [],
options: {},
oneofOptions: {},
create(value?: any): any {
throw new Error('just a stub');
},
Expand Down
5 changes: 4 additions & 1 deletion packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,16 @@ export {
LongType,
RepeatType,
MessageInfo,
JsonOptionsMap,
OneofOptions,
EnumInfo,
FieldInfo,
PartialFieldInfo,
normalizeFieldInfo,
readFieldOptions,
readFieldOption,
readMessageOption
readMessageOption,
readOneofOption
} from './reflection-info';

// Types for messsage objects type at runtime, when concrete type is unknown.
Expand Down
10 changes: 8 additions & 2 deletions packages/runtime/src/message-type-contract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type {FieldInfo, MessageInfo} from "./reflection-info";
import type {FieldInfo, MessageInfo, JsonOptionsMap, OneofOptions} from "./reflection-info";
import type {BinaryReadOptions, BinaryWriteOptions, IBinaryReader, IBinaryWriter} from "./binary-format-contract";
import type {JsonValue} from "./json-typings";
import type {JsonReadOptions, JsonWriteOptions, JsonWriteStringOptions} from "./json-format-contract";
Expand Down Expand Up @@ -56,7 +56,13 @@ export interface IMessageType<T extends object> extends MessageInfo {
/**
* Contains custom message options from the .proto source in JSON format.
*/
readonly options: { [extensionName: string]: JsonValue };
readonly options: JsonOptionsMap;

/**
* Contains custom oneof options from the .proto source in JSON format
* indexed by oneof name.
*/
readonly oneofOptions: OneofOptions;


/**
Expand Down
18 changes: 10 additions & 8 deletions packages/runtime/src/message-type.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type {IMessageType, PartialMessage} from "./message-type-contract";
import type {FieldInfo, PartialFieldInfo} from "./reflection-info";
import type {JsonOptionsMap, OneofOptions, FieldInfo, PartialFieldInfo} from "./reflection-info";
import {normalizeFieldInfo} from "./reflection-info";
import {ReflectionTypeCheck} from "./reflection-type-check";
import {ReflectionJsonReader} from "./reflection-json-reader";
Expand Down Expand Up @@ -45,10 +45,16 @@ export class MessageType<T extends object> implements IMessageType<T> {
readonly fields: readonly FieldInfo[];

/**
* Contains custom service options from the .proto source in JSON format.
* Contains custom message options from the .proto source in JSON format.
*/
readonly options: JsonOptionsMap;

/**
* Contains custom oneof options from the .proto source in JSON format
* indexed by oneof name.
*/
readonly oneofOptions: OneofOptions;


protected readonly defaultCheckDepth = 16;
protected readonly refTypeCheck: ReflectionTypeCheck;
Expand All @@ -57,10 +63,11 @@ export class MessageType<T extends object> implements IMessageType<T> {
protected readonly refBinReader: ReflectionBinaryReader;
protected readonly refBinWriter: ReflectionBinaryWriter;

constructor(name: string, fields: readonly PartialFieldInfo[], options?: JsonOptionsMap) {
constructor(name: string, fields: readonly PartialFieldInfo[], options?: JsonOptionsMap, oneofOptions?: OneofOptions) {
this.typeName = name;
this.fields = fields.map(normalizeFieldInfo);
this.options = options ?? {};
this.oneofOptions = oneofOptions ?? {};
this.refTypeCheck = new ReflectionTypeCheck(this);
this.refJsonReader = new ReflectionJsonReader(this);
this.refJsonWriter = new ReflectionJsonWriter(this);
Expand Down Expand Up @@ -258,8 +265,3 @@ export class MessageType<T extends object> implements IMessageType<T> {
}

}


type JsonOptionsMap = {
[extensionName: string]: JsonValue;
};
58 changes: 55 additions & 3 deletions packages/runtime/src/reflection-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,21 @@ export type EnumInfo = readonly [
string?
];

/**
* Contains custom options from the .proto source in JSON format
* indexed by extension name.
*/
export interface JsonOptionsMap {
[extensionName: string]: JsonValue;
}

/**
* Contains custom oneof options from the .proto source in JSON format
* indexed by oneof name.
*/
export interface OneofOptions {
readonly [oneof: string]: JsonOptionsMap | undefined;
}

/**
* Describes a protobuf message for runtime reflection.
Expand Down Expand Up @@ -89,7 +104,13 @@ export interface MessageInfo {
/**
* Contains custom message options from the .proto source in JSON format.
*/
readonly options: { [extensionName: string]: JsonValue };
readonly options: JsonOptionsMap;

/**
* Contains custom oneof options from the .proto source in JSON format
* indexed by oneof name.
*/
readonly oneofOptions: OneofOptions;

}

Expand All @@ -99,8 +120,9 @@ export interface MessageInfo {
* to be omitted:
* - "fields": omitting means the message has no fields
* - "options": omitting means the message has no options
* - "oneofOptions": omitting means the message has no oneof options
*/
export type PartialMessageInfo = PartialPartial<MessageInfo, "fields" | "options">;
export type PartialMessageInfo = PartialPartial<MessageInfo, "fields" | "options" | "oneofOptions">;

// Make all properties in T optional, except those whose keys are in the union K.
type PartialPartial<T, K extends keyof T> = Partial<Pick<T, K>> & Omit<T, K>;
Expand Down Expand Up @@ -193,7 +215,7 @@ interface fiShared {
/**
* Contains custom field options from the .proto source in JSON format.
*/
options?: { [extensionName: string]: JsonValue };
options?: JsonOptionsMap;

}

Expand Down Expand Up @@ -494,3 +516,33 @@ export function readMessageOption<T extends object>(messageType: MessageInfo, ex
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}

/**
* Read a custom oneof option.
*
* ```proto
* message MyMessage {
* oneof my_oneof {
* option (acme.oneof_opt) = true;
* int32 my_field = 1;
* }
* }
* ```
*
* ```typescript
* let val = readOneofOption(MyMessage, 'my_oneof', 'acme.oneof_opt')
* ```
*/
export function readOneofOption<T extends object>(messageType: MessageInfo, oneofName: string, extensionName: string): JsonValue | undefined;
export function readOneofOption<T extends object>(messageType: MessageInfo, oneofName: string, extensionName: string, extensionType: IMessageType<T>): T | undefined;
export function readOneofOption<T extends object>(messageType: MessageInfo, oneofName: string, extensionName: string, extensionType?: IMessageType<T>): T | JsonValue | undefined {
const options = messageType.oneofOptions[oneofName];
if (!options) {
return undefined;
}
const optionVal = options[extensionName];
if (optionVal === undefined) {
return optionVal;
}
return extensionType ? extensionType.fromJson(optionVal) : optionVal;
}
Loading