Start adding json schema import
This commit is contained in:
@@ -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 { DefinitionEditorComponent } from './definition-editor/definition-editor.component';
|
||||
import { ProtoDefinitionService } from './proto-definition.service';
|
||||
import { JSONSchema7 } from 'json-schema';
|
||||
import { JsonSchemaImporterService } from './json-schema-importer.service';
|
||||
|
||||
declare const __TAURI__: any;
|
||||
|
||||
@@ -30,18 +32,21 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
||||
selector: 'app-proto-definition-selector',
|
||||
imports: [MatButtonModule, MatIconModule, MatListModule, MatTreeModule],
|
||||
template: `
|
||||
<h2>Protobuf Definitions</h2>
|
||||
<h2>Schemas</h2>
|
||||
<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>
|
||||
@if(currentFiles().length > 0) {
|
||||
<button mat-button (click)="exportConfiguration()">
|
||||
Export Configuration
|
||||
</button>
|
||||
}
|
||||
<button mat-button (click)="configurationSelector.click()">
|
||||
Import Configuration
|
||||
</button>
|
||||
<mat-selection-list [multiple]="false">
|
||||
@for (item of currentFiles(); track $index) {
|
||||
<mat-list-option
|
||||
@@ -82,6 +87,12 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
||||
(change)="addDefinitionFiles()"
|
||||
accept=".proto"
|
||||
/>
|
||||
<input
|
||||
#jsonSchemaSelector
|
||||
type="file"
|
||||
(change)="importJsonSchema()"
|
||||
accept=".json"
|
||||
/>
|
||||
<input
|
||||
#configurationSelector
|
||||
type="file"
|
||||
@@ -94,6 +105,7 @@ const collator = new Intl.Collator(undefined, { numeric: true });
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
})
|
||||
export class ProtoDefinitionSelectorComponent {
|
||||
private jsonSchemaImporterService = inject(JsonSchemaImporterService);
|
||||
private protoDefinitionService = inject(ProtoDefinitionService);
|
||||
private snackBar = inject(MatSnackBar);
|
||||
private dialog = inject(MatDialog);
|
||||
@@ -102,6 +114,8 @@ export class ProtoDefinitionSelectorComponent {
|
||||
|
||||
protected protoSelector =
|
||||
viewChild<ElementRef<HTMLInputElement>>('protoSelector');
|
||||
protected jsonSchemaSelector =
|
||||
viewChild<ElementRef<HTMLInputElement>>('jsonSchemaSelector');
|
||||
protected configurationSelector = viewChild<ElementRef<HTMLInputElement>>(
|
||||
'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() {
|
||||
const configurationFile = this.configurationSelector()?.nativeElement.files;
|
||||
try {
|
||||
|
||||
@@ -383,7 +383,6 @@ export class ProtoDefinitionService {
|
||||
switch (fieldType) {
|
||||
case 'string':
|
||||
return StringMessage();
|
||||
break;
|
||||
case 'int32':
|
||||
case 'int64':
|
||||
case 'float':
|
||||
|
||||
Reference in New Issue
Block a user