diff --git a/angular.json b/angular.json index b96e52d..93b38d9 100644 --- a/angular.json +++ b/angular.json @@ -58,7 +58,8 @@ } ], "outputHashing": "all", - "serviceWorker": "ngsw-config.json" + "serviceWorker": "ngsw-config.json", + "security": { "autoCsp": true } }, "development": { "optimization": false, diff --git a/src/app/proto-definition-selector/json-schema-importer.service.ts b/src/app/proto-definition-selector/json-schema-importer.service.ts new file mode 100644 index 0000000..5d16be7 --- /dev/null +++ b/src/app/proto-definition-selector/json-schema-importer.service.ts @@ -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 { + 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'); + } +} diff --git a/src/app/proto-definition-selector/proto-definition-selector.component.ts b/src/app/proto-definition-selector/proto-definition-selector.component.ts index 37a7efd..69092da 100644 --- a/src/app/proto-definition-selector/proto-definition-selector.component.ts +++ b/src/app/proto-definition-selector/proto-definition-selector.component.ts @@ -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: ` -

Protobuf Definitions

+

Schemas

+ + @if(currentFiles().length > 0) { } - @for (item of currentFiles(); track $index) { + >('protoSelector'); + protected jsonSchemaSelector = + viewChild>('jsonSchemaSelector'); protected configurationSelector = viewChild>( '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 { diff --git a/src/app/proto-definition-selector/proto-definition.service.ts b/src/app/proto-definition-selector/proto-definition.service.ts index ec26a39..c9f9c69 100644 --- a/src/app/proto-definition-selector/proto-definition.service.ts +++ b/src/app/proto-definition-selector/proto-definition.service.ts @@ -383,7 +383,6 @@ export class ProtoDefinitionService { switch (fieldType) { case 'string': return StringMessage(); - break; case 'int32': case 'int64': case 'float':