Skip to content

Commit

Permalink
Add message branding with types (#147)
Browse files Browse the repository at this point in the history
  • Loading branch information
odashevskii-plaid authored Sep 2, 2021
1 parent 710b516 commit 088283f
Show file tree
Hide file tree
Showing 6 changed files with 99 additions and 7 deletions.
38 changes: 34 additions & 4 deletions packages/plugin/src/message-type-extensions/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ export class Create implements CustomMethodGenerator {
// const message = { boolField: false, ... };
this.makeMessageVariable(source, descriptor),

// (message as unknown as MessageTypeContainer<ScalarValuesMessage>)[MESSAGE_TYPE] = this;
this.makeTypeAssignment(source, descriptor),

// if (value !== undefined)
// reflectionMergePartial<ScalarValuesMessage>(message, value, this);
this.makeMergeIf(source, descriptor),
Expand Down Expand Up @@ -70,7 +73,7 @@ export class Create implements CustomMethodGenerator {
}


makeMessageVariable(source: TypescriptFile,descriptor: DescriptorProto) {
makeMessageVariable(source: TypescriptFile, descriptor: DescriptorProto) {
let messageType = this.interpreter.getMessageType(descriptor);
let defaultMessage = messageType.create();
return ts.createVariableStatement(
Expand All @@ -87,16 +90,43 @@ export class Create implements CustomMethodGenerator {
}


makeMergeIf(source: TypescriptFile,descriptor: DescriptorProto) {
const MessageInterface = this.imports.type(source,descriptor);
makeTypeAssignment(source: TypescriptFile, descriptor: DescriptorProto) {
const
MessageTypeContainer = this.imports.name(source, 'MessageTypeContainer', this.options.runtimeImportPath, true),
MESSAGE_TYPE = this.imports.name(source, 'MESSAGE_TYPE', this.options.runtimeImportPath),
MessageInterface = this.imports.type(source, descriptor);
;

return ts.createExpressionStatement(
ts.createAssignment(
ts.createElementAccess(
ts.createAsExpression(
ts.createAsExpression(
ts.createIdentifier("message"),
ts.createKeywordTypeNode(ts.SyntaxKind.UnknownKeyword),
),
ts.createTypeReferenceNode(MessageTypeContainer, [
ts.createTypeReferenceNode(MessageInterface, undefined),
]),
),
ts.createIdentifier(MESSAGE_TYPE)
),
ts.createThis(),
),
);
}


makeMergeIf(source: TypescriptFile, descriptor: DescriptorProto) {
const MessageInterface = this.imports.type(source, descriptor);
return ts.createIf(
ts.createBinary(
ts.createIdentifier("value"),
ts.createToken(ts.SyntaxKind.ExclamationEqualsEqualsToken),
ts.createIdentifier("undefined")
),
ts.createExpressionStatement(ts.createCall(
ts.createIdentifier(this.imports.name(source,'reflectionMergePartial', this.options.runtimeImportPath)),
ts.createIdentifier(this.imports.name(source, 'reflectionMergePartial', this.options.runtimeImportPath)),
[ts.createTypeReferenceNode(
MessageInterface,
undefined
Expand Down
41 changes: 41 additions & 0 deletions packages/runtime/spec/reflection-contains-message-type.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import {MessageType, ScalarType, containsMessageType, MESSAGE_TYPE} from '../src';

describe('containsMessageType', () => {
interface MyMessage {
stringField: string;
}

const MyMessage: MessageType<MyMessage> = new MessageType<MyMessage>('.test.MyMessage', [
{no: 1, name: 'string_field', kind: "scalar", T: ScalarType.STRING},
]);

it('allows to extract type after .create', () => {
const msg = MyMessage.create({
stringField: "hello world",
});

if (containsMessageType(msg)) {
expect(msg[MESSAGE_TYPE]).toEqual(MyMessage);
} else {
fail("containsMessageType() must return true");
}
});

it('allows to extract type after .clone', () => {
const cloned = MyMessage.clone(MyMessage.create({
stringField: "hello world",
}));

if (containsMessageType(cloned)) {
expect(cloned[MESSAGE_TYPE]).toEqual(MyMessage);
} else {
fail("containsMessageType() must return true");
}
});

it('returns false if provided a non-message', () => {
const msg: MyMessage = { stringField: "foo" };

expect(containsMessageType(msg)).toBeFalse();
});
});
3 changes: 2 additions & 1 deletion packages/runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export {
} from './json-format-contract';

// Message type contract
export {IMessageType, PartialMessage} from './message-type-contract';
export {IMessageType, PartialMessage, MESSAGE_TYPE} from './message-type-contract';

// Message type implementation via reflection
export {MessageType} from './message-type';
Expand Down Expand Up @@ -78,6 +78,7 @@ export {ReflectionBinaryReader} from './reflection-binary-reader';
export {ReflectionBinaryWriter} from './reflection-binary-writer';
export {ReflectionJsonReader} from './reflection-json-reader';
export {ReflectionJsonWriter} from './reflection-json-writer';
export {containsMessageType, MessageTypeContainer} from './reflection-contains-message-type';

// Oneof helpers
export {isOneofGroup, setOneofValue, getOneofValue, clearOneofValue, getSelectedOneofValue} from './oneof';
Expand Down
3 changes: 3 additions & 0 deletions packages/runtime/src/message-type-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@ import type {BinaryReadOptions, BinaryWriteOptions, IBinaryReader, IBinaryWriter
import type {JsonValue} from "./json-typings";
import type {JsonReadOptions, JsonWriteOptions, JsonWriteStringOptions} from "./json-format-contract";

/** The symbol used as a key on message objects to store the message type. */
export const MESSAGE_TYPE: unique symbol = Symbol("protobuf-ts/message-type");

/**
* Similar to `Partial<T>`, but recursive, and keeps `oneof` groups
* intact.
Expand Down
15 changes: 15 additions & 0 deletions packages/runtime/src/reflection-contains-message-type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {IMessageType, MESSAGE_TYPE} from './message-type-contract';

/**
* The interface that models storing type in a symbol property.
*/
export interface MessageTypeContainer<T extends object> {
[MESSAGE_TYPE]: IMessageType<T>;
}

/**
* Check if the provided object is a proto message.
*/
export function containsMessageType<T extends object>(msg: T): msg is (T & MessageTypeContainer<T>) {
return (msg as MessageTypeContainer<T>)[MESSAGE_TYPE] != null;
}
6 changes: 4 additions & 2 deletions packages/runtime/src/reflection-create.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import type {MessageInfo} from "./reflection-info";
import {reflectionScalarDefault} from "./reflection-scalar-default";
import type {UnknownMessage, UnknownOneofGroup} from "./unknown-types";

import {MESSAGE_TYPE} from './message-type-contract';

/**
* Creates an instance of the generic message, using the field
* information.
*/
export function reflectionCreate(info: MessageInfo): any {
let msg: UnknownMessage = {};
let msg: UnknownMessage = {
[MESSAGE_TYPE]: info,
};
for (let field of info.fields) {
let name = field.localName;
if (field.opt)
Expand Down

0 comments on commit 088283f

Please sign in to comment.