Start adding json schema import
This commit is contained in:
@@ -58,7 +58,8 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"outputHashing": "all",
|
"outputHashing": "all",
|
||||||
"serviceWorker": "ngsw-config.json"
|
"serviceWorker": "ngsw-config.json",
|
||||||
|
"security": { "autoCsp": true }
|
||||||
},
|
},
|
||||||
"development": {
|
"development": {
|
||||||
"optimization": false,
|
"optimization": false,
|
||||||
|
|||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
BooleanMessage,
|
||||||
|
EnumMessage,
|
||||||
|
ListMessage,
|
||||||
|
MessageConfiguration,
|
||||||
|
NumericMessage,
|
||||||
|
ObjectMessage,
|
||||||
|
ProtoMessage,
|
||||||
|
ProtoMessageField,
|
||||||
|
StringMessage,
|
||||||
|
UnknownProto,
|
||||||
|
} from '../model/proto-message.model';
|
||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
|
||||||
|
// jSON Schema types: https://json-schema.org/understanding-json-schema/reference/type
|
||||||
|
// Note: There are specific keywords for each type that should be considered.
|
||||||
|
|
||||||
|
// Ignore required properties for now, we don't care about this yet (in protobuf everything is optional
|
||||||
|
// anyway)
|
||||||
|
// Also ignore references/don't follow them, they can be unknown/raw json. Later can add an option to follow
|
||||||
|
// trusted/untrusted references. Only use $ref that starts with # as it's a local ref/defined in the file.
|
||||||
|
// Also ignore pattern properties: https://tour.json-schema.org/content/03-Objects/01-Pattern-Properties
|
||||||
|
// Also ignore conditional validation: https://tour.json-schema.org/content/05-Conditional-Validation/01-Ensuring-Conditional-Property-Presence
|
||||||
|
|
||||||
|
// Bare minimum to support is anything required by ingey
|
||||||
|
|
||||||
|
// Definitely do want to consider reusable subschemas though, already have a similar check to this
|
||||||
|
// in protobuf parsing as well when a nested message is used: https://tour.json-schema.org/content/06-Combining-Subschemas/01-Reusing-and-Referencing-with-defs-and-ref
|
||||||
|
// Simple example schema (create a test for this):
|
||||||
|
// {
|
||||||
|
// "$schema": "https://json-schema.org/draft/2020-12/schema",
|
||||||
|
// "$id": "https://example.com/product.schema.json",
|
||||||
|
// "title": "Product",
|
||||||
|
// "description": "A product from Acme's catalog",
|
||||||
|
// "type": "object",
|
||||||
|
// "properties": {
|
||||||
|
// "productId": {
|
||||||
|
// "description": "The unique identifier for a product",
|
||||||
|
// "type": "integer"
|
||||||
|
// },
|
||||||
|
// "productName": {
|
||||||
|
// "description": "Name of the product",
|
||||||
|
// "type": "string"
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class JsonSchemaImporterService {
|
||||||
|
async parseJsonSchema(
|
||||||
|
fileName: string,
|
||||||
|
schema: JSONSchema7
|
||||||
|
): Promise<ProtoMessage[]> {
|
||||||
|
const rootElements: ProtoMessage[] = [];
|
||||||
|
if (schema['$defs']) {
|
||||||
|
// Parse all definitions first, they're referenced later
|
||||||
|
for (const [name, value] of Object.entries(schema.$defs)) {
|
||||||
|
if (value instanceof Boolean) {
|
||||||
|
throw new Error('Cannot use boolean for definition value');
|
||||||
|
}
|
||||||
|
const jsonValue = value as JSONSchema7;
|
||||||
|
const objectMessage = ObjectMessage({ name, fileName, values: [] });
|
||||||
|
const properties = jsonValue.properties;
|
||||||
|
if (!properties) {
|
||||||
|
throw new Error('Malformed JOSN Schema, no properties present');
|
||||||
|
}
|
||||||
|
for (const [name, value] of Object.entries(properties)) {
|
||||||
|
objectMessage.messageDefinition.values.push({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
value as JSONSchema7,
|
||||||
|
rootElements,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!schema.title || schema.type !== 'object') {
|
||||||
|
throw new Error('Unsupported JSON Schema, root must be an object');
|
||||||
|
}
|
||||||
|
const properties = schema.properties;
|
||||||
|
if (!properties) {
|
||||||
|
throw new Error('Malformed JOSN Schema, no properties present');
|
||||||
|
}
|
||||||
|
const objectMessage: ObjectMessage = ObjectMessage({
|
||||||
|
name: schema.title!,
|
||||||
|
fileName,
|
||||||
|
values: [],
|
||||||
|
});
|
||||||
|
for (const [name, value] of Object.entries(properties)) {
|
||||||
|
objectMessage.messageDefinition.values.push({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
value as JSONSchema7,
|
||||||
|
rootElements,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
name: schema.title,
|
||||||
|
fileName,
|
||||||
|
values: [{ name: schema.title, configuration: objectMessage }],
|
||||||
|
},
|
||||||
|
...rootElements,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert each type here?
|
||||||
|
private convertMessageRecursive(
|
||||||
|
value: JSONSchema7,
|
||||||
|
parsedMessages: ProtoMessage[],
|
||||||
|
name?: string
|
||||||
|
): MessageConfiguration {
|
||||||
|
if (value.$ref) {
|
||||||
|
const refMessageName = value.$ref.substring('#/definitions/'.length);
|
||||||
|
const foundMessage = parsedMessages.find(
|
||||||
|
(message) => message.name === refMessageName
|
||||||
|
);
|
||||||
|
return ObjectMessage(foundMessage!);
|
||||||
|
}
|
||||||
|
const type = Array.isArray(value.type) ? value.type[0] : value.type;
|
||||||
|
switch (type) {
|
||||||
|
case 'string':
|
||||||
|
if (value.enum) {
|
||||||
|
// TODO: Validate we have strings
|
||||||
|
return EnumMessage(value.enum.map((value) => value as string));
|
||||||
|
}
|
||||||
|
return StringMessage();
|
||||||
|
case 'integer':
|
||||||
|
case 'number':
|
||||||
|
return NumericMessage();
|
||||||
|
case 'array':
|
||||||
|
if (Array.isArray(value.items)) {
|
||||||
|
throw new Error('Cannot parse array type');
|
||||||
|
}
|
||||||
|
return ListMessage(
|
||||||
|
this.convertMessageRecursive(
|
||||||
|
value.items as JSONSchema7,
|
||||||
|
parsedMessages
|
||||||
|
)
|
||||||
|
);
|
||||||
|
case 'boolean':
|
||||||
|
return BooleanMessage();
|
||||||
|
case 'object':
|
||||||
|
return ObjectMessage({
|
||||||
|
name: name!,
|
||||||
|
values: Object.entries(value.properties!).map(([name, value]) => ({
|
||||||
|
name,
|
||||||
|
configuration: this.convertMessageRecursive(
|
||||||
|
// TODO: How can this be a boolean???
|
||||||
|
value as JSONSchema7,
|
||||||
|
parsedMessages,
|
||||||
|
name
|
||||||
|
),
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
throw new Error('Invalid message type found');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,8 @@ import { writeTextFile } from '@tauri-apps/api/fs';
|
|||||||
import { ProtoMessage } from '../model/proto-message.model';
|
import { ProtoMessage } from '../model/proto-message.model';
|
||||||
import { DefinitionEditorComponent } from './definition-editor/definition-editor.component';
|
import { DefinitionEditorComponent } from './definition-editor/definition-editor.component';
|
||||||
import { ProtoDefinitionService } from './proto-definition.service';
|
import { ProtoDefinitionService } from './proto-definition.service';
|
||||||
|
import { JSONSchema7 } from 'json-schema';
|
||||||
|
import { JsonSchemaImporterService } from './json-schema-importer.service';
|
||||||
|
|
||||||
declare const __TAURI__: any;
|
declare const __TAURI__: any;
|
||||||
|
|
||||||
@@ -30,18 +32,21 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
selector: 'app-proto-definition-selector',
|
selector: 'app-proto-definition-selector',
|
||||||
imports: [MatButtonModule, MatIconModule, MatListModule, MatTreeModule],
|
imports: [MatButtonModule, MatIconModule, MatListModule, MatTreeModule],
|
||||||
template: `
|
template: `
|
||||||
<h2>Protobuf Definitions</h2>
|
<h2>Schemas</h2>
|
||||||
<button mat-button (click)="protoSelector.click()">
|
<button mat-button (click)="protoSelector.click()">
|
||||||
Select definitions
|
Import Protobuf Definition
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="jsonSchemaSelector.click()">
|
||||||
|
Import JSON Schema
|
||||||
|
</button>
|
||||||
|
<button mat-button (click)="configurationSelector.click()">
|
||||||
|
Import Configuration
|
||||||
</button>
|
</button>
|
||||||
@if(currentFiles().length > 0) {
|
@if(currentFiles().length > 0) {
|
||||||
<button mat-button (click)="exportConfiguration()">
|
<button mat-button (click)="exportConfiguration()">
|
||||||
Export Configuration
|
Export Configuration
|
||||||
</button>
|
</button>
|
||||||
}
|
}
|
||||||
<button mat-button (click)="configurationSelector.click()">
|
|
||||||
Import Configuration
|
|
||||||
</button>
|
|
||||||
<mat-selection-list [multiple]="false">
|
<mat-selection-list [multiple]="false">
|
||||||
@for (item of currentFiles(); track $index) {
|
@for (item of currentFiles(); track $index) {
|
||||||
<mat-list-option
|
<mat-list-option
|
||||||
@@ -82,6 +87,12 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
(change)="addDefinitionFiles()"
|
(change)="addDefinitionFiles()"
|
||||||
accept=".proto"
|
accept=".proto"
|
||||||
/>
|
/>
|
||||||
|
<input
|
||||||
|
#jsonSchemaSelector
|
||||||
|
type="file"
|
||||||
|
(change)="importJsonSchema()"
|
||||||
|
accept=".json"
|
||||||
|
/>
|
||||||
<input
|
<input
|
||||||
#configurationSelector
|
#configurationSelector
|
||||||
type="file"
|
type="file"
|
||||||
@@ -94,6 +105,7 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
})
|
})
|
||||||
export class ProtoDefinitionSelectorComponent {
|
export class ProtoDefinitionSelectorComponent {
|
||||||
|
private jsonSchemaImporterService = inject(JsonSchemaImporterService);
|
||||||
private protoDefinitionService = inject(ProtoDefinitionService);
|
private protoDefinitionService = inject(ProtoDefinitionService);
|
||||||
private snackBar = inject(MatSnackBar);
|
private snackBar = inject(MatSnackBar);
|
||||||
private dialog = inject(MatDialog);
|
private dialog = inject(MatDialog);
|
||||||
@@ -102,6 +114,8 @@ export class ProtoDefinitionSelectorComponent {
|
|||||||
|
|
||||||
protected protoSelector =
|
protected protoSelector =
|
||||||
viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
||||||
|
protected jsonSchemaSelector =
|
||||||
|
viewChild<ElementRef<HTMLInputElement>>('jsonSchemaSelector');
|
||||||
protected configurationSelector = viewChild<ElementRef<HTMLInputElement>>(
|
protected configurationSelector = viewChild<ElementRef<HTMLInputElement>>(
|
||||||
'configurationSelector'
|
'configurationSelector'
|
||||||
);
|
);
|
||||||
@@ -175,6 +189,29 @@ export class ProtoDefinitionSelectorComponent {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async importJsonSchema() {
|
||||||
|
const jsonSchemaFile = this.jsonSchemaSelector()?.nativeElement.files;
|
||||||
|
try {
|
||||||
|
const fileContents = await jsonSchemaFile?.item(0)?.text();
|
||||||
|
if (fileContents) {
|
||||||
|
const schema: JSONSchema7 = JSON.parse(fileContents);
|
||||||
|
const messageObjects =
|
||||||
|
await this.jsonSchemaImporterService.parseJsonSchema(
|
||||||
|
jsonSchemaFile?.item(0)?.name!,
|
||||||
|
schema
|
||||||
|
);
|
||||||
|
this.allProtoFiles.set(messageObjects);
|
||||||
|
this.selectedDefinition.set(messageObjects);
|
||||||
|
this.selectedProtoFile.set(null);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(err);
|
||||||
|
this.snackBar.open(
|
||||||
|
"Could not parse json schema file, please ensure it's valid. Note: This feature is still unstable and is likely to have errors with valid schemas."
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected async importConfiguration() {
|
protected async importConfiguration() {
|
||||||
const configurationFile = this.configurationSelector()?.nativeElement.files;
|
const configurationFile = this.configurationSelector()?.nativeElement.files;
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -383,7 +383,6 @@ export class ProtoDefinitionService {
|
|||||||
switch (fieldType) {
|
switch (fieldType) {
|
||||||
case 'string':
|
case 'string':
|
||||||
return StringMessage();
|
return StringMessage();
|
||||||
break;
|
|
||||||
case 'int32':
|
case 'int32':
|
||||||
case 'int64':
|
case 'int64':
|
||||||
case 'float':
|
case 'float':
|
||||||
|
|||||||
Reference in New Issue
Block a user