Added JSON validation for imports.

This commit is contained in:
PeekabooSteam 2023-12-27 16:26:31 +00:00
parent de14f6c738
commit 955057183d
7 changed files with 503 additions and 71 deletions

View File

@ -1366,7 +1366,7 @@
"length": "9536"
}
],
"TACAN": "",
"TACAN": "0X",
"ICAO": "HECW",
"elevation": "439"
}

View File

@ -36,7 +36,7 @@
},
"magHeading": {
"type": "string",
"pattern": "^([0-2][0-9]{2})|(3[0-6][0-9])?$"
"pattern": "^([0-2][0-9]{2})|(3(([0-5][0-9])|(60)))?$"
}
},
"required": ["magHeading"]

View File

@ -0,0 +1,377 @@
{
"$defs": {
"coalitionName": {
"enum": [
"blue",
"neutral",
"red"
],
"type": "string"
},
"lat": {
"maximum": 90,
"minimum": -90,
"type": "number"
},
"lng": {
"maximum": 180,
"minimum": -180,
"type": "number"
},
"vec2": {
"additionalProperties": false,
"properties": {
"lat": {
"$ref": "#/$defs/lat"
},
"lng": {
"$ref": "#/$defs/lng"
}
},
"required": [
"lat",
"lng"
],
"type": "object"
},
"vec3": {
"additionalProperties": false,
"properties": {
"alt": {
"type": "number"
},
"lat": {
"$ref": "#/$defs/lat"
},
"lng": {
"$ref": "#/$defs/lng"
}
},
"required": [
"alt",
"lat",
"lng"
],
"type": "object"
}
},
"patternProperties": {
".*": {
"items": {
"additionalProperties": false,
"properties": {
"activePath": {
"items": {
"$ref": "#/$defs/vec3"
},
"type": "array"
},
"alive": {
"type": "boolean"
},
"ammo": {
"items": {
"additionalProperties": false,
"properties": {
"category": {
"minimum": 0,
"type": "number"
},
"guidance": {
"minimum": 0,
"type": "number"
},
"missileCategory": {
"minimum": 0,
"type": "number"
},
"name": {
"minLength": 3,
"type": "string"
},
"quantity": {
"minimum": 0,
"type": "number"
}
},
"required": [
"quantity",
"name",
"guidance",
"category",
"missileCategory"
],
"type": "object"
},
"type": "array"
},
"category": {
"type": "string"
},
"categoryDisplayName": {
"type": "string"
},
"coalition": {
"$ref": "#/$defs/coalitionName"
},
"contacts": {
"type": "array"
},
"controlled": {
"type": "boolean"
},
"country": {
"type": "number"
},
"desiredAltitude": {
"minimum": 0,
"type": "number"
},
"desiredAltitudeType": {
"enum": [
"AGL",
"ASL"
],
"type": "string"
},
"desiredSpeed": {
"minimum": 0,
"type": "number"
},
"desiredSpeedType": {
"enum": [
"CAS",
"GS"
],
"type": "string"
},
"emissionsCountermeasures": {
"enum": [
"attac",
"defend",
"free",
"silent"
],
"type": "string"
},
"followRoads": {
"type": "boolean"
},
"formationOffset": {
"additionalProperties": false,
"properties": {
"x": {
"minimum": 0,
"type": "number"
},
"y": {
"minimum": 0,
"type": "number"
},
"z": {
"minimum": 0,
"type": "number"
}
},
"required": [
"x",
"y",
"z"
],
"type": "object"
},
"fuel": {
"maximum": 100,
"minimum": 0,
"type": "number"
},
"generalSettings": {
"additionalProperties": false,
"properties": {
"prohibitAA": {
"type": "boolean"
},
"prohibitAfterburner": {
"type": "boolean"
},
"prohibitAG": {
"type": "boolean"
},
"prohibitAirWpn": {
"type": "boolean"
},
"prohibitJettison": {
"type": "boolean"
}
},
"required": [
"prohibitAA",
"prohibitAfterburner",
"prohibitAG",
"prohibitAirWpn",
"prohibitJettison"
],
"type": "object"
},
"groupName": {
"type": "string"
},
"hasTask": {
"type": "boolean"
},
"heading": {
"type": "number"
},
"health": {
"maximum": 100,
"minimum": 0,
"type": "number"
},
"horizontalVelocity": {
"minimum": 0,
"type": "number"
},
"human": {
"type": "boolean"
},
"ID": {
"type": "number"
},
"isActiveAWACS": {
"type": "boolean"
},
"isActiveTanker": {
"type": "boolean"
},
"isLeader": {
"type": "boolean"
},
"leaderID": {
"minimum": 0,
"type": "number"
},
"name": {
"type": "string"
},
"onOff": {
"type": "boolean"
},
"operateAs": {
"$ref": "#/$defs/coalitionName"
},
"position": {
"$ref": "#/$defs/vec3"
},
"radio": {
"additionalProperties": false,
"properties": {
"callsign": {
"type": "number"
},
"callsignNumber": {
"type": "number"
},
"frequency": {
"type": "number"
}
},
"required": [
"frequency",
"callsign",
"callsignNumber"
],
"type": "object"
},
"reactionToThreat": {
"enum": [
"evade",
"maneouvre",
"none",
"passive"
],
"type": "string"
},
"ROE": {
"enum": [
"designated",
"free",
"hold",
"return"
],
"type": "string"
},
"shotsIntensity": {
"maximum": 3,
"minimum": 1,
"type": "number"
},
"shotsScatter": {
"maximum": 3,
"minimum": 1,
"type": "number"
},
"speed": {
"minimum": 0,
"type": "number"
},
"state": {
"type": "string"
},
"TACAN": {
"properties": {
"callsign": {
"type": "string"
},
"channel": {
"minimum": 0,
"type": "number"
},
"isOn": {
"type": "boolean"
},
"XY": {
"enum": [
"X",
"Y"
],
"type": "string"
}
},
"required": [
"callsign",
"channel",
"isOn",
"XY"
],
"type": "object"
},
"targetID": {
"minimum": 0,
"type": "number"
},
"targetPosition": {
"$ref": "#/$defs/vec2"
},
"task": {
"type": "string"
},
"track": {
"type": "number"
},
"unitName": {
"type": "string"
},
"verticalVelocity": {
"minimum": 0,
"type": "number"
}
},
"type": "object",
"required": ["activePath","alive","ammo","category","categoryDisplayName","coalition","contacts","controlled","country","desiredAltitude","desiredAltitudeType","desiredSpeed","desiredSpeedType","emissionsCountermeasures","followRoads","formationOffset","fuel","generalSettings","groupName","hasTask","heading","health","horizontalVelocity","human","ID","isActiveAWACS","isActiveTanker","isLeader","leaderID","name","onOff","operateAs","position","radio","reactionToThreat","ROE","shotsIntensity","shotsScatter","speed","state","TACAN","targetID","targetPosition","task","track","unitName","verticalVelocity"]
},
"minItems": 1,
"type": "array"
}
},
"type": "object"
}

View File

@ -1,34 +1,55 @@
import Ajv, { JSONSchemaType } from "ajv";
import { AnySchemaObject, Schema } from "ajv/dist/core";
import Ajv from "ajv";
import { AnySchemaObject } from "ajv/dist/core";
// For future extension
abstract class JSONSchemaValidator {
#ajv:Ajv;
#compiledValidator:any;
#schema!:AnySchemaObject;
constructor( schema:AnySchemaObject ) {
this.#schema = schema;
this.#ajv = new Ajv({
"allErrors": true
});
this.#compiledValidator = this.getAjv().compile(this.getSchema());
}
getAjv() {
return this.#ajv;
}
getCompiledValidator() {
return this.#compiledValidator;
}
getErrors() {
return this.getCompiledValidator().errors;
}
getSchema() {
return this.#schema;
}
validate(data:any) {
return (this.getCompiledValidator())(data);
}
}
export class AirbasesJSONSchemaValidator extends JSONSchemaValidator {
constructor( ) {
constructor() {
const schema = require("../schemas/airbases.schema.json");
super( schema );
const ajv = new Ajv({
"allErrors": true
});
[
require( "../../public/databases/airbases/caucasus.json" ),
@ -41,11 +62,21 @@ export class AirbasesJSONSchemaValidator extends JSONSchemaValidator {
require( "../../public/databases/airbases/syria.json" ),
require( "../../public/databases/airbases/thechannel.json" )
].forEach( data => {
const validate = ajv.compile(this.getSchema());
const validate = this.getAjv().compile(this.getSchema());
const valid = validate(data);
if (!valid) console.error(validate.errors);
});
}
}
export class ImportFileJSONSchemaValidator extends JSONSchemaValidator {
constructor() {
const schema = require("../schemas/importdata.schema.json");
super( schema );
}
}

View File

@ -25,6 +25,10 @@ export class UnitDataFileExport extends UnitDataFile {
* Show the form to start the export journey
*/
showForm(units: Unit[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
const unitCanBeExported = (unit: Unit) => !["Aircraft", "Helicopter"].includes(unit.getCategory());

View File

@ -1,6 +1,7 @@
import { getApp } from "../..";
import { Dialog } from "../../dialog/dialog";
import { UnitData } from "../../interfaces";
import { ImportFileJSONSchemaValidator } from "../../schemas/schema";
import { UnitDataFile } from "./unitdatafile";
export class UnitDataFileImport extends UnitDataFile {
@ -48,21 +49,59 @@ export class UnitDataFileImport extends UnitDataFile {
unitsManager.spawnUnits(category, unitsToSpawn, coalition, false);
}
}
/*
for (let groupName in groups) {
if (groupName !== "" && groups[groupName].length > 0 && (groups[groupName].every((unit: UnitData) => { return unit.category == "GroundUnit"; }) || groups[groupName].every((unit: any) => { return unit.category == "NavyUnit"; }))) {
var aliveUnits = groups[groupName].filter((unit: UnitData) => { return unit.alive });
var units = aliveUnits.map((unit: UnitData) => {
return { unitType: unit.name, location: unit.position, liveryID: "" }
});
getApp().getUnitsManager().spawnUnits(groups[groupName][0].category, units, groups[groupName][0].coalition, true);
selectFile() {
var input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e: any) => {
var file = e.target.files[0];
if (!file) {
return;
}
}
//*/
var reader = new FileReader();
reader.onload = (e: any) => {
try {
this.#fileData = JSON.parse(e.target.result);
const validator = new ImportFileJSONSchemaValidator();
if (!validator.validate(this.#fileData)) {
const errors = validator.getErrors().reduce((acc:any, error:any) => {
acc.push(error.instancePath + ": " + error.message)
return acc;
}, [] as string[]);
this.#showFileDataErrors(errors);
} else {
this.#showForm();
}
} catch(e:any) {
this.#showFileDataErrors([e]);
}
};
reader.readAsText(file);
})
input.click();
}
#showFileDataErrors( reasons:string[]) {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "hide");
});
const reasonsList = this.dialog.getElement().querySelector(".import-error-reasons");
if (reasonsList instanceof HTMLElement)
reasonsList.innerHTML = `<li>${reasons.join("</li><li>")}</li>`;
this.dialog.show();
}
#showForm() {
this.dialog.getElement().querySelectorAll("[data-on-error]").forEach((el:Element) => {
el.classList.toggle("hide", el.getAttribute("data-on-error") === "show");
});
const data: any = {};
for (const [group, units] of Object.entries(this.#fileData)) {
@ -87,44 +126,11 @@ export class UnitDataFileImport extends UnitDataFile {
}
/*
groups.filter((unit:Unit) => unitCanBeImported(unit)).forEach((unit:Unit) => {
const category = unit.getCategoryLabel();
const coalition = unit.getCoalition();
if (!data.hasOwnProperty(category)) {
data[category] = {};
}
if (!data[category].hasOwnProperty(coalition))
data[category][coalition] = [];
data[category][coalition].push(unit);
});
//*/
this.data = data;
this.buildCategoryCoalitionTable();
this.dialog.show();
}
selectFile() {
var input = document.createElement("input");
input.type = "file";
input.addEventListener("change", (e: any) => {
var file = e.target.files[0];
if (!file) {
return;
}
var reader = new FileReader();
reader.onload = (e: any) => {
this.#fileData = JSON.parse(e.target.result);
this.#showForm();
};
reader.readAsText(file);
})
input.click();
}
#unitDataCanBeImported(unitData: UnitData) {
return unitData.alive && this.#unitGroupDataCanBeImported([unitData]);
}

View File

@ -4,26 +4,40 @@
</div>
<div class="ol-dialog-content">
<p><%= textContent %></p>
<% if (showFilenameInput) { %>
<div class="export-filename-container">
<label>Filename:</label>
<input id="export-filename">
<img src="resources/theme/images/icons/keyboard-solid.svg">
</div>
<% } %>
<div class="import-form" data-on-error="hide">
<p><%= textContent %></p>
<% if (showFilenameInput) { %>
<div class="export-filename-container">
<label>Filename:</label>
<input id="export-filename">
<img src="resources/theme/images/icons/keyboard-solid.svg">
</div>
<% } %>
<table class="categories-coalitions">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div>
<table class="categories-coalitions">
<thead>
</thead>
<tbody>
</tbody>
</table>
</div>
<div class="import-errors" data-on-error="show">
<div class="ol-dialog-footer ol-group">
<button class="start-transfer"><%= submitButtonText %></button>
<button data-on-click="closeDialog">Close</button>
<p>Data could not be imported because:</p>
<ul class="import-error-reasons"></ul>
<div>Please correct the error(s) and run the import again.</div>
</div>
<div class="ol-dialog-footer ol-group">
<button class="start-transfer" data-on-error="hide"><%= submitButtonText %></button>
<button data-on-click="closeDialog">Close</button>
</div>
</div>
</div>