Rework proto parsing to use javascript objects, add nested and object message support
Objects are preferred to classes as they're easier to clone and serialise/deserialise
This commit is contained in:
@@ -1,27 +1,80 @@
|
|||||||
export class StringMessage implements MessageConfiguration {
|
export enum MessageTypeEnum {
|
||||||
constructor(public maxLength?: number) {}
|
String = 'string',
|
||||||
|
Boolean = 'boolean',
|
||||||
|
Numeric = 'numeric',
|
||||||
|
List = 'list',
|
||||||
|
Map = 'map',
|
||||||
|
Object = 'object',
|
||||||
|
Raw = 'raw',
|
||||||
}
|
}
|
||||||
|
|
||||||
export class BooleanMessage implements MessageConfiguration {}
|
export interface StringMessage extends MessageConfiguration {
|
||||||
|
maxLength?: number;
|
||||||
export class NumericMessage implements MessageConfiguration {
|
|
||||||
constructor(public min?: number, public max?: number) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ListMessage implements MessageConfiguration {
|
export interface BooleanMessage extends MessageConfiguration {}
|
||||||
constructor(readonly subConfiguration: MessageConfiguration) {}
|
|
||||||
|
export interface NumericMessage extends MessageConfiguration {
|
||||||
|
min?: number;
|
||||||
|
max?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class MapMessage implements MessageConfiguration {
|
export interface ListMessage extends MessageConfiguration {
|
||||||
constructor(
|
subConfiguration: MessageConfiguration;
|
||||||
readonly keyConfiguration: MessageConfiguration,
|
|
||||||
readonly valueConfiguration: MessageConfiguration
|
|
||||||
) {}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ObjectMessage implements MessageConfiguration {}
|
export interface MapMessage extends MessageConfiguration {
|
||||||
|
keyConfiguration: MessageConfiguration;
|
||||||
|
valueConfiguration: MessageConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
export class MessageConfiguration {}
|
export interface ObjectMessage extends MessageConfiguration {
|
||||||
|
messageDefinition: ProtoMessage;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RawMessage extends MessageConfiguration {}
|
||||||
|
|
||||||
|
export interface MessageConfiguration {
|
||||||
|
type: MessageTypeEnum;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StringMessage = (maxLength?: number): StringMessage => ({
|
||||||
|
type: MessageTypeEnum.String,
|
||||||
|
maxLength,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BooleanMessage = (): BooleanMessage => ({
|
||||||
|
type: MessageTypeEnum.Boolean,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const NumericMessage = (min?: number, max?: number): NumericMessage => ({
|
||||||
|
type: MessageTypeEnum.Numeric,
|
||||||
|
min,
|
||||||
|
max,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ListMessage = (
|
||||||
|
subConfiguration: MessageConfiguration
|
||||||
|
): ListMessage => ({
|
||||||
|
type: MessageTypeEnum.List,
|
||||||
|
subConfiguration,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MapMessage = (
|
||||||
|
keyConfiguration: MessageConfiguration,
|
||||||
|
valueConfiguration: MessageConfiguration
|
||||||
|
): MapMessage => ({
|
||||||
|
type: MessageTypeEnum.Map,
|
||||||
|
keyConfiguration,
|
||||||
|
valueConfiguration,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ObjectMessage = (messageDefinition: ProtoMessage) => ({
|
||||||
|
type: MessageTypeEnum.Object,
|
||||||
|
messageDefinition,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const RawMessage = (): RawMessage => ({ type: MessageTypeEnum.Raw });
|
||||||
|
|
||||||
export interface ProtoMessageField {
|
export interface ProtoMessageField {
|
||||||
name: string;
|
name: string;
|
||||||
@@ -33,3 +86,8 @@ export interface ProtoMessage {
|
|||||||
name: string;
|
name: string;
|
||||||
values: ProtoMessageField[];
|
values: ProtoMessageField[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const UnknownProto = (name: string): ProtoMessage => ({
|
||||||
|
name,
|
||||||
|
values: [{ name: 'Raw JSON', configuration: RawMessage(), value: '' }],
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,17 +1,12 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
Type,
|
|
||||||
provideExperimentalZonelessChangeDetection,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
BooleanMessage,
|
|
||||||
ListMessage,
|
ListMessage,
|
||||||
MapMessage,
|
MapMessage,
|
||||||
MessageConfiguration,
|
MessageTypeEnum,
|
||||||
NumericMessage,
|
ObjectMessage,
|
||||||
ProtoMessage,
|
ProtoMessage,
|
||||||
StringMessage,
|
|
||||||
} from './model/proto-message.model';
|
} from './model/proto-message.model';
|
||||||
import { ProtoDefinitionService } from './proto-definition.service';
|
import { ProtoDefinitionService } from './proto-definition.service';
|
||||||
|
|
||||||
@@ -19,15 +14,25 @@ let testProto = `
|
|||||||
syntax="proto3";
|
syntax="proto3";
|
||||||
|
|
||||||
message Test {
|
message Test {
|
||||||
string hello = 1;
|
message NestedMessage {
|
||||||
int32 hello2 = 2;
|
string nested = 1;
|
||||||
int64 hello3 = 3;
|
}
|
||||||
float hello4 = 4;
|
string hello = 1;
|
||||||
double hello5 = 5;
|
int32 hello2 = 2;
|
||||||
bool hello6 = 6;
|
int64 hello3 = 3;
|
||||||
repeated string hello7 = 7;
|
float hello4 = 4;
|
||||||
map<string, string> hello8 = 8;
|
double hello5 = 5;
|
||||||
|
bool hello6 = 6;
|
||||||
|
repeated string hello7 = 7;
|
||||||
|
map<string, string> hello8 = 8;
|
||||||
|
ReferenceMessage hello9 = 9;
|
||||||
|
NestedMessage hello10 = 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message ReferenceMessage {
|
||||||
|
string test = 1;
|
||||||
|
}
|
||||||
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
describe('TestService', () => {
|
describe('TestService', () => {
|
||||||
@@ -42,34 +47,54 @@ describe('TestService', () => {
|
|||||||
|
|
||||||
it('should convert parse protobuf correctly', async () => {
|
it('should convert parse protobuf correctly', async () => {
|
||||||
const testMessages = await service.parseProtoDefinition(testProto);
|
const testMessages = await service.parseProtoDefinition(testProto);
|
||||||
const converted = testMessages[0];
|
const converted = testMessages[1];
|
||||||
|
|
||||||
expect(converted.name).toBe('Test');
|
expect(converted.name).toBe('Test');
|
||||||
|
|
||||||
expect(converted.values.length).toBe(8);
|
expect(converted.values.length).toBe(10);
|
||||||
checkNameAndType(converted, 'hello', StringMessage);
|
checkNameAndType(converted, 'hello', MessageTypeEnum.String);
|
||||||
checkNameAndType(converted, 'hello2', NumericMessage);
|
checkNameAndType(converted, 'hello2', MessageTypeEnum.Numeric);
|
||||||
checkNameAndType(converted, 'hello3', NumericMessage);
|
checkNameAndType(converted, 'hello3', MessageTypeEnum.Numeric);
|
||||||
checkNameAndType(converted, 'hello4', NumericMessage);
|
checkNameAndType(converted, 'hello4', MessageTypeEnum.Numeric);
|
||||||
checkNameAndType(converted, 'hello5', NumericMessage);
|
checkNameAndType(converted, 'hello5', MessageTypeEnum.Numeric);
|
||||||
checkNameAndType(converted, 'hello6', BooleanMessage);
|
checkNameAndType(converted, 'hello6', MessageTypeEnum.Boolean);
|
||||||
checkNameAndType(converted, 'hello7', ListMessage);
|
checkNameAndType(converted, 'hello7', MessageTypeEnum.List);
|
||||||
checkNameAndType(converted, 'hello8', MapMessage);
|
checkNameAndType(converted, 'hello8', MessageTypeEnum.Map);
|
||||||
|
checkNameAndType(converted, 'hello9', MessageTypeEnum.Object);
|
||||||
|
checkNameAndType(converted, 'hello10', MessageTypeEnum.Object);
|
||||||
|
|
||||||
const listMessage = converted.values[6].configuration as ListMessage;
|
const listMessage = converted.values[6].configuration as ListMessage;
|
||||||
expect(listMessage.subConfiguration).toBeInstanceOf(StringMessage);
|
expect(listMessage.subConfiguration.type).toBe(MessageTypeEnum.String);
|
||||||
|
|
||||||
const mapMessage = converted.values[7].configuration as MapMessage;
|
const mapMessage = converted.values[7].configuration as MapMessage;
|
||||||
expect(mapMessage.keyConfiguration).toBeInstanceOf(StringMessage);
|
expect(mapMessage.keyConfiguration.type).toBe(MessageTypeEnum.String);
|
||||||
expect(mapMessage.valueConfiguration).toBeInstanceOf(StringMessage);
|
expect(mapMessage.valueConfiguration.type).toBe(MessageTypeEnum.String);
|
||||||
|
|
||||||
|
const referenceMessage = converted.values[8].configuration as ObjectMessage;
|
||||||
|
expect(referenceMessage.messageDefinition.values.length).toBe(1);
|
||||||
|
expect(referenceMessage.messageDefinition.name).toBe('ReferenceMessage');
|
||||||
|
const nestedReferenceMessage = referenceMessage.messageDefinition.values[0];
|
||||||
|
expect(nestedReferenceMessage.configuration.type).toBe(
|
||||||
|
MessageTypeEnum.String
|
||||||
|
);
|
||||||
|
expect(nestedReferenceMessage.name).toBe('test');
|
||||||
|
|
||||||
|
const nestedMessage = converted.values[9].configuration as ObjectMessage;
|
||||||
|
expect(nestedMessage.messageDefinition.values.length).toBe(1);
|
||||||
|
expect(nestedMessage.messageDefinition.name).toBe('NestedMessage');
|
||||||
|
const nestedNestedMessage = nestedMessage.messageDefinition.values[0];
|
||||||
|
expect(nestedNestedMessage.configuration.type).toBe(MessageTypeEnum.String);
|
||||||
|
expect(nestedNestedMessage.name).toBe('nested');
|
||||||
|
|
||||||
|
// TODO: Enum type
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const checkNameAndType = (
|
const checkNameAndType = (
|
||||||
converted: ProtoMessage,
|
converted: ProtoMessage,
|
||||||
name: string,
|
name: string,
|
||||||
type: Type<MessageConfiguration>
|
type: MessageTypeEnum
|
||||||
) => {
|
) => {
|
||||||
const field = converted.values.find((value) => value.name === name);
|
const field = converted.values.find((value) => value.name === name);
|
||||||
expect(field?.configuration).toBeInstanceOf(type);
|
expect(field?.configuration.type).toBe(type);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,9 +5,13 @@ import {
|
|||||||
ListMessage,
|
ListMessage,
|
||||||
MapMessage,
|
MapMessage,
|
||||||
MessageConfiguration,
|
MessageConfiguration,
|
||||||
|
MessageTypeEnum,
|
||||||
NumericMessage,
|
NumericMessage,
|
||||||
|
ObjectMessage,
|
||||||
ProtoMessage,
|
ProtoMessage,
|
||||||
|
RawMessage,
|
||||||
StringMessage,
|
StringMessage,
|
||||||
|
UnknownProto,
|
||||||
} from './model/proto-message.model';
|
} from './model/proto-message.model';
|
||||||
|
|
||||||
@Injectable({
|
@Injectable({
|
||||||
@@ -18,16 +22,19 @@ export class ProtoDefinitionService {
|
|||||||
|
|
||||||
async parseProtoDefinition(protoContents: string): Promise<ProtoMessage[]> {
|
async parseProtoDefinition(protoContents: string): Promise<ProtoMessage[]> {
|
||||||
const definition = await parse(protoContents);
|
const definition = await parse(protoContents);
|
||||||
const messages = definition.root.nested;
|
const messages = definition.root;
|
||||||
if (messages) {
|
if (messages) {
|
||||||
const objectDefinitions = Object.values(messages);
|
const messageObjects: ProtoMessage[] =
|
||||||
const messageObjects = [];
|
this.parseAllNestedMessages(messages);
|
||||||
for (const objectDefinition of objectDefinitions) {
|
// Now do another pass, where we populate each of the object fields based on their definitions... need a way to detect recursion? Maybe only go up to 5 layers deep?
|
||||||
// TODO: Better way to check for message type (i.e. not enum)?
|
for (const messageObject of messageObjects) {
|
||||||
if ('fieldsArray' in objectDefinition) {
|
for (const value of messageObject.values) {
|
||||||
messageObjects.push(
|
if (value.configuration.type === MessageTypeEnum.Object) {
|
||||||
this.convertProtoDefinitionToModel(objectDefinition)
|
this.populateNestedObject(
|
||||||
);
|
value.configuration as ObjectMessage,
|
||||||
|
messageObjects
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return messageObjects;
|
return messageObjects;
|
||||||
@@ -35,6 +42,46 @@ export class ProtoDefinitionService {
|
|||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private parseAllNestedMessages(obj: ReflectionObject) {
|
||||||
|
const messageObjects: ProtoMessage[] = [];
|
||||||
|
if ('nestedArray' in obj && obj.nestedArray) {
|
||||||
|
const nestedArray = obj.nestedArray as ReflectionObject[];
|
||||||
|
for (const nestedObj of nestedArray) {
|
||||||
|
messageObjects.push(...this.parseAllNestedMessages(nestedObj));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ('fieldsArray' in obj) {
|
||||||
|
messageObjects.push(this.convertProtoDefinitionToModel(obj));
|
||||||
|
}
|
||||||
|
return messageObjects;
|
||||||
|
}
|
||||||
|
|
||||||
|
private populateNestedObject(
|
||||||
|
objectMessage: ObjectMessage,
|
||||||
|
availableMessages: ProtoMessage[]
|
||||||
|
) {
|
||||||
|
for (const message of availableMessages) {
|
||||||
|
if (message.name === objectMessage.messageDefinition.name) {
|
||||||
|
objectMessage.messageDefinition = {
|
||||||
|
name: message.name,
|
||||||
|
values: message.values.map((value) => {
|
||||||
|
if (value.configuration.type === MessageTypeEnum.Object) {
|
||||||
|
this.populateNestedObject(
|
||||||
|
value.configuration as ObjectMessage,
|
||||||
|
availableMessages
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return structuredClone(value);
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
objectMessage.messageDefinition = UnknownProto(
|
||||||
|
objectMessage.messageDefinition.name
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
private convertProtoDefinitionToModel(
|
private convertProtoDefinitionToModel(
|
||||||
messageDefinition?: ReflectionObject
|
messageDefinition?: ReflectionObject
|
||||||
): ProtoMessage {
|
): ProtoMessage {
|
||||||
@@ -44,20 +91,19 @@ export class ProtoDefinitionService {
|
|||||||
values: [],
|
values: [],
|
||||||
};
|
};
|
||||||
const fields = messageDefinition.fieldsArray as FieldBase[];
|
const fields = messageDefinition.fieldsArray as FieldBase[];
|
||||||
// TODO: Needs to be a visitor/tree search/recursive since messages/enums may be nested
|
|
||||||
for (const field of fields) {
|
for (const field of fields) {
|
||||||
let type: MessageConfiguration | undefined;
|
let type: MessageConfiguration | undefined;
|
||||||
if ('rule' in field && field.rule === 'repeated') {
|
if ('rule' in field && field.rule === 'repeated') {
|
||||||
const subType = this.getTypeFromField(field.type);
|
const subType = this.getTypeFromField(field.type);
|
||||||
if (subType) {
|
if (subType) {
|
||||||
type = new ListMessage(subType);
|
type = ListMessage(subType);
|
||||||
}
|
}
|
||||||
} else if (field instanceof MapField) {
|
} else if (field instanceof MapField) {
|
||||||
// Map
|
// Map
|
||||||
const keyConfiguration = this.getTypeFromField(field.keyType);
|
const keyConfiguration = this.getTypeFromField(field.keyType);
|
||||||
const valueConfiguration = this.getTypeFromField(field.type);
|
const valueConfiguration = this.getTypeFromField(field.type);
|
||||||
if (keyConfiguration && valueConfiguration) {
|
if (keyConfiguration && valueConfiguration) {
|
||||||
type = new MapMessage(keyConfiguration, valueConfiguration);
|
type = MapMessage(keyConfiguration, valueConfiguration);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
type = this.getTypeFromField(field.type);
|
type = this.getTypeFromField(field.type);
|
||||||
@@ -82,19 +128,19 @@ export class ProtoDefinitionService {
|
|||||||
): MessageConfiguration | undefined {
|
): MessageConfiguration | undefined {
|
||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return new StringMessage();
|
return StringMessage();
|
||||||
break;
|
break;
|
||||||
case 'int32':
|
case 'int32':
|
||||||
case 'int64':
|
case 'int64':
|
||||||
case 'float':
|
case 'float':
|
||||||
case 'double':
|
case 'double':
|
||||||
return new NumericMessage();
|
return NumericMessage();
|
||||||
break;
|
break;
|
||||||
case 'bool':
|
case 'bool':
|
||||||
return new BooleanMessage();
|
return BooleanMessage();
|
||||||
break;
|
break;
|
||||||
// TODO: bytes as well, though that's pretty useless (can't really represent/edit it?)
|
// TODO: bytes as well, though that's pretty useless (can't really represent/edit it?)
|
||||||
}
|
}
|
||||||
return undefined;
|
return ObjectMessage({ name: fieldType, values: [] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user