Start adding json schema import
All checks were successful
release-nightly / macos (push) Successful in 2m12s
release-nightly / web-demo (push) Successful in 4m59s

This commit is contained in:
2025-05-17 11:03:19 +09:30
parent 1c4536878e
commit 885ed5892e
4 changed files with 210 additions and 7 deletions

View File

@@ -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');
}
}

View File

@@ -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 {

View File

@@ -383,7 +383,6 @@ export class ProtoDefinitionService {
switch (fieldType) {
case 'string':
return StringMessage();
break;
case 'int32':
case 'int64':
case 'float':