Rework definition import to support multiple files and imports, nested messages, add configuration export/import

This commit is contained in:
2024-07-13 22:01:41 +09:30
parent 72c24a70ae
commit cf0a96591c
7 changed files with 330 additions and 82 deletions

View File

@@ -15,56 +15,128 @@ import {
StringMessage,
UnknownProto,
} from '../model/proto-message.model';
import { readTextFile } from '@tauri-apps/api/fs';
@Injectable({
providedIn: 'root',
})
export class ProtoDefinitionService {
constructor() {}
async parseProtoDefinition(protoContents: string): Promise<ProtoMessage[]> {
const definition = await parse(protoContents);
const messages = definition.root;
if (messages) {
const messageObjects: ProtoBase[] = this.parseAllNestedMessages(messages);
const standardMessages = messageObjects
.filter((messageObject) => 'values' in messageObject)
.map((messageObject) => messageObject as ProtoMessage);
for (const messageObject of standardMessages) {
for (const value of messageObject.values) {
if (value.configuration.type === MessageTypeEnum.Object) {
this.populateNestedObject(
value.configuration as ObjectMessage,
standardMessages
async parseAllDefinitions(allProtoFiles: File[]): Promise<ProtoMessage[]> {
const messages: (ProtoBase | undefined)[] = [];
for (const protoFile of allProtoFiles) {
const definition = await parse(await protoFile.text());
// Try resolve imports, if they fail (due to invalid path or running app in browser)
// they'll be skipped
if (definition.imports) {
for (const importPath of definition.imports) {
try {
const fileName = importPath.substring(
importPath.lastIndexOf('/') + 1
);
allProtoFiles.push(
new File(
[
new Blob([await readTextFile(importPath)], {
type: 'text/plain',
}),
],
fileName
)
);
} catch (err) {
console.error(
`Failed to import file ${importPath}. Skipping...`,
err
);
}
}
}
const enumMessages = messageObjects
.filter((messageObject) => 'options' in messageObject)
.map((messageObject) => messageObject as ProtoEnum);
for (const messageObject of standardMessages) {
for (const value of messageObject.values) {
if (value.configuration.type === MessageTypeEnum.Object) {
value.configuration = this.populateEnumMessages(
value.configuration as ObjectMessage,
enumMessages
);
}
}
}
return standardMessages;
const messageDefinitions = definition.root;
const parsedMessages = this.parseAllNestedMessages(
messageDefinitions,
protoFile.name,
definition.package
);
messages.push(...parsedMessages);
}
return [];
if (messages.length === 0) {
return [];
}
// Now check for duplicates in case we auto-imported files
for (let i = 0; i < messages.length - 1; i++) {
for (let j = i + 1; j < messages.length; j++) {
if (messages[i]?.fullName === messages[j]?.fullName) {
messages[j] = undefined;
}
}
}
const distinctMessages = messages.filter(
(message) => message
) as ProtoBase[];
// Then populate all object/enum messages for the final passes
const standardMessages = distinctMessages
.filter((messageObject) => 'values' in messageObject)
.map((messageObject) => messageObject as ProtoMessage);
for (const messageObject of standardMessages) {
for (const value of messageObject.values) {
if (value.configuration.type === MessageTypeEnum.Object) {
this.populateNestedObject(
value.configuration as ObjectMessage,
messageObject.packageName!,
standardMessages
);
}
}
}
const enumMessages = distinctMessages
.filter((messageObject) => 'options' in messageObject)
.map((messageObject) => messageObject as ProtoEnum);
for (const messageObject of standardMessages) {
for (const value of messageObject.values) {
if (value.configuration.type === MessageTypeEnum.Object) {
value.configuration = this.populateEnumMessages(
value.configuration as ObjectMessage,
enumMessages
);
}
}
}
return standardMessages;
}
private populateEnumMessages(
objectMessage: ObjectMessage,
enumMessages: ProtoEnum[]
): MessageConfiguration {
let useFullName = false;
let objectMessageName = objectMessage.messageDefinition.name;
if (objectMessageName.includes('.')) {
// First do a check for current package.
for (const enumMessage of enumMessages) {
if (
enumMessage.packageName ===
objectMessage.messageDefinition.packageName &&
enumMessage.fullName ===
`${objectMessage.messageDefinition.packageName}.${objectMessageName}`
) {
return EnumMessage(enumMessage.options);
}
}
// If there's no match, then we'll be matching on full name, assuming a separate package
useFullName = true;
}
// None matching messages in this package found, check external package
for (const enumMessage of enumMessages) {
if (enumMessage.name === objectMessage.messageDefinition.name) {
if (
(useFullName && enumMessage.fullName === objectMessageName) ||
(!useFullName && enumMessage.name === objectMessageName)
) {
return EnumMessage(enumMessage.options);
}
}
@@ -83,23 +155,34 @@ export class ProtoDefinitionService {
return objectMessage;
}
private parseAllNestedMessages(obj: ReflectionObject) {
private parseAllNestedMessages(
obj: ReflectionObject,
fileName: string,
packageName?: string
) {
const messageObjects: ProtoBase[] = [];
// Nested messages/enums
if ('nestedArray' in obj && obj.nestedArray) {
const nestedArray = obj.nestedArray as ReflectionObject[];
for (const nestedObj of nestedArray) {
messageObjects.push(...this.parseAllNestedMessages(nestedObj));
messageObjects.push(
...this.parseAllNestedMessages(nestedObj, fileName, packageName)
);
}
}
// Message
if ('fieldsArray' in obj) {
messageObjects.push(this.convertProtoDefinitionToModel(obj));
messageObjects.push(
this.convertProtoDefinitionToModel(obj, fileName, packageName)
);
}
// Enum
if ('values' in obj) {
messageObjects.push({
name: obj.name,
fullName: obj.fullName,
packageName,
fileName,
options: Object.entries(obj.values as Record<string, string>)
.sort((a, b) => Number(a[1]) - Number(b[1]))
.map(([key, _]) => key),
@@ -110,36 +193,73 @@ export class ProtoDefinitionService {
private populateNestedObject(
objectMessage: ObjectMessage,
packageName: string,
availableMessages: ProtoMessage[]
) {
let useFullName = false;
let objectMessageName = objectMessage.messageDefinition.name;
if (objectMessageName.includes('.')) {
// First do a check for current package.
for (const message of availableMessages) {
if (
message.packageName === packageName &&
message.fullName === `${packageName}.${objectMessageName}`
) {
this.updateObjectMessage(objectMessage, message, availableMessages);
return;
}
}
// If there's no match, then we'll be matching on full name, assuming a separate package
useFullName = true;
}
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);
}),
};
if (
(useFullName && message.fullName === objectMessageName) ||
(!useFullName && message.name === objectMessageName)
) {
this.updateObjectMessage(objectMessage, message, availableMessages);
return;
}
}
objectMessage.messageDefinition = UnknownProto(
objectMessage.messageDefinition.name
objectMessage.messageDefinition.name,
objectMessage.messageDefinition.fullName
);
}
private updateObjectMessage(
objectMessage: ObjectMessage,
message: ProtoMessage,
availableMessages: ProtoMessage[]
) {
objectMessage.messageDefinition = {
name: objectMessage.messageDefinition.name,
values: message.values.map((value) => {
if (value.configuration.type === MessageTypeEnum.Object) {
this.populateNestedObject(
value.configuration as ObjectMessage,
message.packageName!,
availableMessages
);
}
return structuredClone(value);
}),
};
}
private convertProtoDefinitionToModel(
messageDefinition?: ReflectionObject
messageDefinition: ReflectionObject,
fileName: string,
packageName?: string
): ProtoBase {
if (messageDefinition && 'fieldsArray' in messageDefinition) {
const convertedFields: ProtoMessage = {
name: messageDefinition.name,
fullName: messageDefinition.fullName.substring(1), // trim leading .
packageName,
fileName,
values: [],
};
const fields = messageDefinition.fieldsArray as FieldBase[];