Rework definition import to support multiple files and imports, nested messages, add configuration export/import
This commit is contained in:
@@ -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[];
|
||||
|
||||
Reference in New Issue
Block a user