Start adding editor, fix up sidebar styling
This commit is contained in:
18
angular.json
18
angular.json
@@ -18,18 +18,21 @@
|
||||
"outputPath": "dist/proto-creator",
|
||||
"index": "src/index.html",
|
||||
"browser": "src/main.ts",
|
||||
"polyfills": [
|
||||
],
|
||||
"polyfills": [],
|
||||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": ["src/assets"],
|
||||
"styles": ["src/styles.scss"]
|
||||
"styles": [
|
||||
"src/styles.scss",
|
||||
"highlight.js/styles/atom-one-dark.min.css"
|
||||
],
|
||||
"allowedCommonJsDependencies": ["protobufjs"]
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "500kb",
|
||||
"maximumWarning": "700kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
@@ -69,8 +72,7 @@
|
||||
"test": {
|
||||
"builder": "@angular-devkit/build-angular:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
],
|
||||
"polyfills": [],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"inlineStyleLanguage": "scss",
|
||||
"assets": [
|
||||
@@ -79,9 +81,7 @@
|
||||
"input": "public"
|
||||
}
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.scss"
|
||||
],
|
||||
"styles": ["src/styles.scss"],
|
||||
"scripts": []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
"test": "ng test",
|
||||
"build": "ng build",
|
||||
"watch": "ng build --watch --configuration development",
|
||||
"tauri": "tauri"
|
||||
@@ -21,6 +22,7 @@
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@tauri-apps/api": "^1.5.6",
|
||||
"highlight.js": "^11.9.0",
|
||||
"protobufjs": "~7.3.0",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
|
||||
@@ -46,7 +46,7 @@ fn build_menu(app_name: &str) -> Menu {
|
||||
}
|
||||
let open_folder =
|
||||
CustomMenuItem::new("openfolder".to_string(), "Open Folder").accelerator("cmd+shift+O");
|
||||
let file_submenu = Menu::new()
|
||||
let mut file_submenu = Menu::new()
|
||||
.add_item(open_folder)
|
||||
.add_native_item(MenuItem::CloseWindow);
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
|
||||
@@ -1,12 +1,42 @@
|
||||
<mat-toolbar data-tauri-drag-region color="secondary"
|
||||
><h1>BufPiv</h1></mat-toolbar
|
||||
>
|
||||
<mat-toolbar data-tauri-drag-region color="secondary">
|
||||
<h1>BufPiv</h1>
|
||||
<span>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="leftSideOpen.set(!leftSideOpen())"
|
||||
title="Toggle left drawer"
|
||||
>
|
||||
<mat-icon [class.filled]="leftSideOpen()">dock_to_right</mat-icon>
|
||||
</button>
|
||||
<button
|
||||
mat-icon-button
|
||||
(click)="rightSideOpen.set(!rightSideOpen())"
|
||||
title="Toggle right drawer"
|
||||
>
|
||||
<mat-icon [class.filled]="rightSideOpen()">dock_to_left</mat-icon>
|
||||
</button>
|
||||
</span>
|
||||
</mat-toolbar>
|
||||
<mat-sidenav-container autosize>
|
||||
@if(selectedDirectory()) {
|
||||
<mat-sidenav mode="side" opened>
|
||||
<mat-sidenav
|
||||
[mode]="isMobile() ? 'over' : 'side'"
|
||||
[opened]="leftSideOpen()"
|
||||
(closed)="leftSideOpen.set(false)"
|
||||
>
|
||||
<app-file-tree [files]="files()"></app-file-tree>
|
||||
</mat-sidenav>
|
||||
}
|
||||
<mat-sidenav
|
||||
[opened]="rightSideOpen()"
|
||||
[mode]="isMobile() ? 'over' : 'side'"
|
||||
position="end"
|
||||
(closed)="rightSideOpen.set(false)"
|
||||
>
|
||||
<app-proto-definition-selector
|
||||
(messageSelected)="selectMessage($event)"
|
||||
></app-proto-definition-selector>
|
||||
</mat-sidenav>
|
||||
<mat-sidenav-content>
|
||||
@if (!selectedDirectory()) {
|
||||
<div
|
||||
@@ -21,7 +51,8 @@
|
||||
Open Folder
|
||||
</button>
|
||||
</div>
|
||||
}@else { }
|
||||
}@else {
|
||||
<app-editor [selectedMessage]="selectedMessage()"></app-editor>
|
||||
}
|
||||
</mat-sidenav-content>
|
||||
<!-- TODO: Proto selector (add/remove proto definitinos, select definition for current file) on right side -->
|
||||
</mat-sidenav-container>
|
||||
|
||||
@@ -8,8 +8,18 @@ mat-toolbar {
|
||||
flex: 0 0 auto;
|
||||
padding-top: 24px;
|
||||
--mat-toolbar-standard-height: 70px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
mat-sidenav-container {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mat-sidenav.mat-drawer-end {
|
||||
padding: var(--mat-sidenav-container-shape);
|
||||
}
|
||||
|
||||
.mat-icon.filled {
|
||||
font-variation-settings: "FILL" 1, "wght" 400, "GRAD" 0, "opsz" 24;
|
||||
}
|
||||
|
||||
@@ -1,19 +1,25 @@
|
||||
import { BreakpointObserver, Breakpoints } from '@angular/cdk/layout';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatSidenavModule } from '@angular/material/sidenav';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { message, open } from '@tauri-apps/api/dialog';
|
||||
import { FileEntry, readDir } from '@tauri-apps/api/fs';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
import { MatTreeModule } from '@angular/material/tree';
|
||||
import { RouterOutlet } from '@angular/router';
|
||||
import { message, open } from '@tauri-apps/api/dialog';
|
||||
import { UnlistenFn, listen } from '@tauri-apps/api/event';
|
||||
import { FileEntry, readDir } from '@tauri-apps/api/fs';
|
||||
import { EditorComponent } from './editor/editor.component';
|
||||
import {
|
||||
FileOrFolder,
|
||||
FileTreeComponent,
|
||||
} from './file-tree/file-tree.component';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
import { OpenFolderMessage } from './messages/openfolder.message';
|
||||
import { ProtoMessage } from './model/proto-message.model';
|
||||
import { ProtoDefinitionSelectorComponent } from './proto-definition-selector/proto-definition-selector.component';
|
||||
const mobileBreakpoints = [Breakpoints.Handset, Breakpoints.TabletPortrait];
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -29,14 +35,30 @@ import { OpenFolderMessage } from './messages/openfolder.message';
|
||||
MatTreeModule,
|
||||
FileTreeComponent,
|
||||
MatIconModule,
|
||||
EditorComponent,
|
||||
ProtoDefinitionSelectorComponent,
|
||||
],
|
||||
})
|
||||
export class AppComponent {
|
||||
protected selectedDirectory = signal<string | null>(null);
|
||||
protected files = signal<FileOrFolder[]>([]);
|
||||
protected selectedMessage = signal<ProtoMessage | undefined>(undefined);
|
||||
protected rightSideOpen = signal(true);
|
||||
protected leftSideOpen = signal(true);
|
||||
|
||||
private unlisten?: UnlistenFn;
|
||||
|
||||
protected isMobile = signal(
|
||||
this.breakpointObserver.isMatched(mobileBreakpoints)
|
||||
);
|
||||
|
||||
constructor(private breakpointObserver: BreakpointObserver) {
|
||||
breakpointObserver
|
||||
.observe(mobileBreakpoints)
|
||||
.pipe(takeUntilDestroyed())
|
||||
.subscribe((matches) => this.isMobile.set(matches.matches));
|
||||
}
|
||||
|
||||
async ngOnInit() {
|
||||
this.unlisten = await listen(
|
||||
'openfolder',
|
||||
@@ -83,9 +105,14 @@ export class AppComponent {
|
||||
children: entry.children
|
||||
?.sort(this.sortFiles(splitNumbers))
|
||||
.map((entry) => this.mapEntry(entry, splitNumbers)),
|
||||
path: entry.path,
|
||||
};
|
||||
}
|
||||
|
||||
private sortFiles = (splitNumbers: RegExp) => (a: FileEntry, b: FileEntry) =>
|
||||
a.name?.localeCompare(b.name ?? '') ?? 0;
|
||||
|
||||
protected selectMessage(message: ProtoMessage) {
|
||||
this.selectedMessage.set(message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
APP_INITIALIZER,
|
||||
ApplicationConfig,
|
||||
provideExperimentalZonelessChangeDetection,
|
||||
} from '@angular/core';
|
||||
@@ -6,11 +7,39 @@ import { provideRouter } from '@angular/router';
|
||||
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { routes } from './app.routes';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import json from 'highlight.js/lib/languages/json';
|
||||
import {
|
||||
MAT_ICON_DEFAULT_OPTIONS,
|
||||
MatIconRegistry,
|
||||
} from '@angular/material/icon';
|
||||
|
||||
export const appConfig: ApplicationConfig = {
|
||||
providers: [
|
||||
provideExperimentalZonelessChangeDetection(),
|
||||
provideRouter(routes),
|
||||
provideAnimationsAsync(),
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useValue: () => {
|
||||
hljs.registerLanguage('json', json);
|
||||
},
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: (iconRegistry: MatIconRegistry) => () => {
|
||||
iconRegistry.registerFontClassAlias(
|
||||
'material-symbols-rounded',
|
||||
'material-symbols-rounded'
|
||||
);
|
||||
},
|
||||
deps: [MatIconRegistry],
|
||||
multi: true,
|
||||
},
|
||||
{
|
||||
provide: MAT_ICON_DEFAULT_OPTIONS,
|
||||
useValue: { fontSet: 'material-symbols-rounded' },
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
@@ -1 +1,3 @@
|
||||
<textarea [(ngModel)]="fileContents" disabled></textarea>
|
||||
<!-- <div>@for (item of items; track $index) {} @switch () {}</div> -->
|
||||
|
||||
<pre><code class="language-json" #code>{{fileContents()}}</code></pre>
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { Component, input } from '@angular/core';
|
||||
import { Component, ElementRef, effect, input, viewChild } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { readTextFile } from '@tauri-apps/api/fs';
|
||||
import { parse } from 'protobufjs';
|
||||
import hljs from 'highlight.js/lib/core';
|
||||
import { ProtoMessage } from '../model/proto-message.model';
|
||||
import { ProtoDefinitionService } from '../proto-definition.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-editor',
|
||||
@@ -13,18 +14,15 @@ import { parse } from 'protobufjs';
|
||||
})
|
||||
export class EditorComponent {
|
||||
fileContents = input<string>();
|
||||
selectedProtoPath = input<string>();
|
||||
selectedMessage = input<ProtoMessage>();
|
||||
private code = viewChild<ElementRef<HTMLElement>>('code');
|
||||
|
||||
async loadProtoDefinition() {
|
||||
try {
|
||||
const protoPath = this.selectedProtoPath();
|
||||
if (protoPath) {
|
||||
const protoContents = await readTextFile(protoPath);
|
||||
const definition = await parse(protoContents);
|
||||
console.log(definition);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
constructor(private protoDefinitionService: ProtoDefinitionService) {
|
||||
effect(() => {
|
||||
const element = this.code()?.nativeElement;
|
||||
if (element) {
|
||||
hljs.highlightElement(element);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
:host {
|
||||
display: block;
|
||||
}
|
||||
23
src/app/file-tree/file-tree.component.html
Normal file
23
src/app/file-tree/file-tree.component.html
Normal file
@@ -0,0 +1,23 @@
|
||||
<mat-tree [dataSource]="dataSource()" [treeControl]="treeControl">
|
||||
<mat-tree-node
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodePadding
|
||||
(click)="fileSelected.emit(node.file)"
|
||||
>
|
||||
<button mat-icon-button disabled></button>
|
||||
{{ node.file.name }}
|
||||
</mat-tree-node>
|
||||
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodeToggle>
|
||||
<button
|
||||
mat-icon-button
|
||||
matTreeNodePadding
|
||||
[attr.aria-label]="'Toggle ' + node.file.name"
|
||||
>
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
@if (treeControl.isExpanded(node)) { expand_more } @else { chevron_right
|
||||
}
|
||||
</mat-icon>
|
||||
</button>
|
||||
{{ node.file.name }}
|
||||
</mat-tree-node>
|
||||
</mat-tree>
|
||||
@@ -19,6 +19,7 @@ export interface FileOrFolder {
|
||||
isDirectory: boolean;
|
||||
name: string;
|
||||
children?: FileOrFolder[];
|
||||
path: string;
|
||||
}
|
||||
|
||||
interface FileNode {
|
||||
@@ -31,29 +32,7 @@ interface FileNode {
|
||||
selector: 'app-file-tree',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatTreeModule, MatIconModule, MatButtonModule],
|
||||
template: `<mat-tree [dataSource]="dataSource()" [treeControl]="treeControl">
|
||||
<mat-tree-node
|
||||
*matTreeNodeDef="let node"
|
||||
matTreeNodePadding
|
||||
(click)="fileSelected.emit(node.file)"
|
||||
>
|
||||
<button mat-icon-button disabled></button>
|
||||
{{ node.file.name }}
|
||||
</mat-tree-node>
|
||||
<mat-tree-node *matTreeNodeDef="let node; when: hasChild" matTreeNodeToggle>
|
||||
<button
|
||||
mat-icon-button
|
||||
matTreeNodePadding
|
||||
[attr.aria-label]="'Toggle ' + node.file.name"
|
||||
>
|
||||
<mat-icon class="mat-icon-rtl-mirror">
|
||||
@if (treeControl.isExpanded(node)) { expand_more } @else {
|
||||
chevron_right }
|
||||
</mat-icon>
|
||||
</button>
|
||||
{{ node.file.name }}
|
||||
</mat-tree-node>
|
||||
</mat-tree>`,
|
||||
templateUrl: './file-tree.component.html',
|
||||
styleUrl: './file-tree.component.scss',
|
||||
})
|
||||
export class FileTreeComponent {
|
||||
|
||||
19
src/app/model/proto-message.model.ts
Normal file
19
src/app/model/proto-message.model.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
export enum MessageType {
|
||||
String,
|
||||
Boolean,
|
||||
Numeric,
|
||||
List,
|
||||
Map,
|
||||
Object,
|
||||
}
|
||||
|
||||
export interface ProtoMessageField {
|
||||
name: string;
|
||||
type: MessageType;
|
||||
value: any;
|
||||
}
|
||||
|
||||
export interface ProtoMessage {
|
||||
name: string;
|
||||
values: ProtoMessageField[];
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
:host {
|
||||
display: block;
|
||||
border-radius: var(--mat-sidenav-container-shape);
|
||||
padding: var(--mat-sidenav-container-shape);
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
transition: background-color 100ms linear;
|
||||
}
|
||||
|
||||
:host.droppable * {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
input {
|
||||
display: none;
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
import { MediaMatcher } from '@angular/cdk/layout';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import {
|
||||
ChangeDetectionStrategy,
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
ElementRef,
|
||||
HostBinding,
|
||||
HostListener,
|
||||
OnDestroy,
|
||||
output,
|
||||
signal,
|
||||
viewChild,
|
||||
} from '@angular/core';
|
||||
import { MatListModule } from '@angular/material/list';
|
||||
import { ProtoMessage } from '../model/proto-message.model';
|
||||
import { ProtoDefinitionService } from '../proto-definition.service';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
|
||||
@Component({
|
||||
selector: 'app-proto-definition-selector',
|
||||
standalone: true,
|
||||
imports: [CommonModule, MatListModule, MatButtonModule],
|
||||
template: `
|
||||
<h2>Protobuf Definitions</h2>
|
||||
<mat-list>
|
||||
@for (item of definitionFiles(); track $index) {
|
||||
<mat-list-item (click)="selectProtoDefinition(item)">{{
|
||||
item.name
|
||||
}}</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
<button mat-button (click)="protoSelector.click()">
|
||||
Select definitions
|
||||
</button>
|
||||
<input
|
||||
#protoSelector
|
||||
type="file"
|
||||
(change)="addDefinitionFiles()"
|
||||
accept=".proto"
|
||||
/>
|
||||
<mat-list>
|
||||
@for (item of selectedDefinition(); track $index) {
|
||||
<mat-list-item (click)="messageSelected.emit(item)">{{
|
||||
item.name
|
||||
}}</mat-list-item>
|
||||
}
|
||||
</mat-list>
|
||||
<!-- TODO: more detail when dragging over so user knows they can drop the file -->
|
||||
`,
|
||||
styleUrl: './proto-definition-selector.component.css',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProtoDefinitionSelectorComponent {
|
||||
protoSelector = viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
||||
messageSelected = output<ProtoMessage>();
|
||||
|
||||
protected definitionFiles = signal<File[]>([]);
|
||||
protected selectedDefinition = signal<ProtoMessage[]>([]);
|
||||
protected isDragging = signal(false);
|
||||
|
||||
private currentFiles: string[] = [];
|
||||
|
||||
@HostBinding('class.droppable')
|
||||
get droppable() {
|
||||
return this.isDragging();
|
||||
}
|
||||
|
||||
constructor(
|
||||
private protoDefinitionService: ProtoDefinitionService,
|
||||
private elementRef: ElementRef
|
||||
) {}
|
||||
|
||||
protected async addDefinitionFiles() {
|
||||
const files = this.protoSelector()?.nativeElement.files;
|
||||
if (files) {
|
||||
const definitionFiles = this.definitionFiles();
|
||||
for (let i = 0; i < files.length; i++) {
|
||||
if (!this.currentFiles.includes(files[i].name)) {
|
||||
definitionFiles.push(files[i]);
|
||||
this.currentFiles.push(files[i].name);
|
||||
}
|
||||
}
|
||||
this.definitionFiles.set(definitionFiles);
|
||||
}
|
||||
}
|
||||
|
||||
protected async selectProtoDefinition(file: File) {
|
||||
try {
|
||||
const protoContents = await file.text();
|
||||
const messageObjects =
|
||||
await this.protoDefinitionService.parseProtoDefinition(protoContents);
|
||||
this.selectedDefinition.set(messageObjects);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
alert(
|
||||
"Failed to parse protobuf definition, please check it's a valid file."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@HostListener('dragover', ['$event'])
|
||||
onDrag(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
@HostListener('drop', ['$event'])
|
||||
onDrop(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const protoSelector = this.protoSelector();
|
||||
if (protoSelector) {
|
||||
protoSelector.nativeElement.files = event.dataTransfer?.files ?? null;
|
||||
}
|
||||
this.isDragging.set(false);
|
||||
this.addDefinitionFiles();
|
||||
}
|
||||
|
||||
@HostListener('dragenter')
|
||||
onDragEnter() {
|
||||
this.isDragging.set(true);
|
||||
}
|
||||
|
||||
@HostListener('dragleave', ['$event'])
|
||||
onDragLeave(event: DragEvent) {
|
||||
if (event.target) this.isDragging.set(false);
|
||||
}
|
||||
}
|
||||
57
src/app/proto-definition.service.spec.ts
Normal file
57
src/app/proto-definition.service.spec.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
|
||||
import { provideExperimentalZonelessChangeDetection } from '@angular/core';
|
||||
import { MessageType, ProtoMessage } from './model/proto-message.model';
|
||||
import { ProtoDefinitionService } from './proto-definition.service';
|
||||
|
||||
let testProto = `
|
||||
syntax="proto3";
|
||||
|
||||
message Test {
|
||||
string hello = 1;
|
||||
int32 hello2 = 2;
|
||||
int64 hello3 = 3;
|
||||
float hello4 = 4;
|
||||
double hello5 = 5;
|
||||
bool hello6 = 6;
|
||||
repeated string hello7 = 7;
|
||||
map<string, string> hello8 = 8;
|
||||
}
|
||||
`;
|
||||
|
||||
describe('TestService', () => {
|
||||
let service: ProtoDefinitionService;
|
||||
|
||||
beforeEach(() => {
|
||||
TestBed.configureTestingModule({
|
||||
providers: [provideExperimentalZonelessChangeDetection()],
|
||||
});
|
||||
service = TestBed.inject(ProtoDefinitionService);
|
||||
});
|
||||
|
||||
it('should convert parse protobuf correctly', async () => {
|
||||
const testMessages = await service.parseProtoDefinition(testProto);
|
||||
const converted = testMessages[0];
|
||||
|
||||
expect(converted.name).toBe('Test');
|
||||
|
||||
expect(converted.values.length).toBe(8);
|
||||
checkNameAndType(converted, 'hello', MessageType.String);
|
||||
checkNameAndType(converted, 'hello2', MessageType.Numeric);
|
||||
checkNameAndType(converted, 'hello3', MessageType.Numeric);
|
||||
checkNameAndType(converted, 'hello4', MessageType.Numeric);
|
||||
checkNameAndType(converted, 'hello5', MessageType.Numeric);
|
||||
checkNameAndType(converted, 'hello6', MessageType.Boolean);
|
||||
checkNameAndType(converted, 'hello7', MessageType.List);
|
||||
checkNameAndType(converted, 'hello8', MessageType.Map);
|
||||
});
|
||||
});
|
||||
|
||||
const checkNameAndType = (
|
||||
converted: ProtoMessage,
|
||||
name: string,
|
||||
type: MessageType
|
||||
) => {
|
||||
const field = converted.values.find((value) => value.name === name);
|
||||
expect(field?.type).toBe(type);
|
||||
};
|
||||
70
src/app/proto-definition.service.ts
Normal file
70
src/app/proto-definition.service.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FieldBase, MapField, ReflectionObject, parse } from 'protobufjs';
|
||||
import { MessageType, ProtoMessage } from './model/proto-message.model';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class ProtoDefinitionService {
|
||||
constructor() {}
|
||||
|
||||
async parseProtoDefinition(protoContents: string): Promise<ProtoMessage[]> {
|
||||
const definition = await parse(protoContents);
|
||||
const messages = definition.root.nested;
|
||||
if (messages) {
|
||||
const objectDefinitions = Object.values(messages);
|
||||
const messageObjects = [];
|
||||
for (const objectDefinition of objectDefinitions) {
|
||||
// TODO: Better way to check for message type (i.e. not enum)?
|
||||
if ('fieldsArray' in objectDefinition) {
|
||||
messageObjects.push(
|
||||
this.convertProtoDefinitionToModel(objectDefinition)
|
||||
);
|
||||
}
|
||||
}
|
||||
return messageObjects;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
private convertProtoDefinitionToModel(
|
||||
messageDefinition?: ReflectionObject
|
||||
): ProtoMessage {
|
||||
if (messageDefinition && 'fieldsArray' in messageDefinition) {
|
||||
const convertedFields: ProtoMessage = {
|
||||
name: messageDefinition.name,
|
||||
values: [],
|
||||
};
|
||||
const fields = messageDefinition.fieldsArray as FieldBase[];
|
||||
// TODO: Needs to be a visitor/tree search since messages/enums may be nested
|
||||
for (const field of fields) {
|
||||
// TODO: Better way to check this?
|
||||
if ('rule' in field && field.rule === 'repeated') {
|
||||
// Repeated/list field
|
||||
} else if (field instanceof MapField) {
|
||||
// Map
|
||||
} else {
|
||||
switch (field.type) {
|
||||
case 'string':
|
||||
break;
|
||||
case 'int32':
|
||||
case 'int64':
|
||||
case 'float':
|
||||
case 'double':
|
||||
break;
|
||||
case 'bool':
|
||||
break;
|
||||
// TODO: bytes as well, though that's pretty useless (can't really represent/edit it?)
|
||||
}
|
||||
}
|
||||
convertedFields.values.push({
|
||||
name: field.name,
|
||||
type: MessageType.String,
|
||||
value: '',
|
||||
});
|
||||
}
|
||||
return convertedFields;
|
||||
}
|
||||
throw 'ReflectionObject is not a message';
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>Test</title>
|
||||
<base href="/">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap" rel="stylesheet">
|
||||
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<base href="/" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<link rel="icon" type="image/x-icon" href="favicon.ico" />
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500&display=swap"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Material+Symbols+Rounded:opsz,wght,FILL,GRAD@20..48,100..700,0..1,-50..200"
|
||||
rel="stylesheet"
|
||||
/>
|
||||
</head>
|
||||
<body class="mat-typography">
|
||||
<app-root></app-root>
|
||||
</body>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -5,7 +5,16 @@
|
||||
|
||||
@mixin custom-colours($theme) {
|
||||
.mat-toolbar {
|
||||
background-color: mat.get-theme-color($theme, primary-container);
|
||||
background-color: mat.get-theme-color($theme, tertiary-container);
|
||||
}
|
||||
|
||||
app-proto-definition-selector {
|
||||
background-color: mat.get-theme-color($theme, secondary-container);
|
||||
transition: background-color 100ms linear;
|
||||
}
|
||||
|
||||
.droppable {
|
||||
background-color: mat.get-theme-color($theme, secondary, 40);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
12
test.proto
Normal file
12
test.proto
Normal file
@@ -0,0 +1,12 @@
|
||||
syntax="proto3";
|
||||
|
||||
message Test {
|
||||
string hello = 1;
|
||||
int32 hello2 = 2;
|
||||
int64 hello3 = 3;
|
||||
float hello4 = 4;
|
||||
double hello5 = 5;
|
||||
bool hello6 = 6;
|
||||
repeated string hello7 = 7;
|
||||
map<string, string> hello8 = 8;
|
||||
}
|
||||
Reference in New Issue
Block a user