Merge pull request #788 from Pax1601/779-add-json-validation

779 add json validation
This commit is contained in:
Pax1601
2024-01-03 09:13:38 +01:00
committed by GitHub
10 changed files with 698 additions and 68 deletions

View File

@@ -46,6 +46,7 @@
"@types/node": "^18.16.1",
"@types/sortablejs": "^1.15.0",
"@types/svg-injector": "^0.0.29",
"ajv": "^8.12.0",
"babelify": "^10.0.0",
"browserify": "^17.0.0",
"concurrently": "^7.6.0",

View File

@@ -1037,7 +1037,7 @@
"01R": {
"magHeading": "11",
"Heading": "16",
"ILS": "109.7"
"ILS": "109.70"
}
},
{

View File

@@ -60,11 +60,6 @@
padding: 0;
}
.leaflet-container img.leaflet-tile {
/* See: https://bugs.chromium.org/p/chromium/issues/detail?id=600120 */
mix-blend-mode: plus-lighter;
}
.leaflet-container.leaflet-touch-zoom {
-ms-touch-action: pan-x pan-y;
touch-action: pan-x pan-y;
@@ -651,7 +646,7 @@ svg.leaflet-image-layer.leaflet-interactive path {
}
/* Printing */
@media print {
/* Prevent printers from removing background-images of controls. */
.leaflet-control {

View File

@@ -18,6 +18,7 @@ import { Manager } from "./other/manager";
import { SVGInjector } from "@tanem/svg-injector";
import { ServerManager } from "./server/servermanager";
import { sha256 } from 'js-sha256';
import Ajv from "ajv"
import { BLUE_COMMANDER, FILL_SELECTED_RING, GAME_MASTER, HIDE_UNITS_SHORT_RANGE_RINGS, RED_COMMANDER, SHOW_UNITS_ACQUISITION_RINGS, SHOW_UNITS_ENGAGEMENT_RINGS, SHOW_UNIT_LABELS } from "./constants/constants";
import { aircraftDatabase } from "./unit/databases/aircraftdatabase";
@@ -27,6 +28,8 @@ import { navyUnitDatabase } from "./unit/databases/navyunitdatabase";
import { UnitListPanel } from "./panels/unitlistpanel";
import { ContextManager } from "./context/contextmanager";
import { Context } from "./context/context";
import { AirDefenceUnitSpawnMenu } from "./controls/unitspawnmenu";
import { AirbasesJSONSchemaValidator } from "./schemas/schema";
var VERSION = "{{OLYMPUS_VERSION_NUMBER}}";
@@ -193,6 +196,9 @@ export class OlympusApp {
this.#unitsManager = new UnitsManager();
this.#weaponsManager = new WeaponsManager();
/* Validate data */
this.#validateData();
// Toolbars
this.getToolbarsManager().add("primaryToolbar", new PrimaryToolbar("primary-toolbar"))
.add("commandModeToolbar", new CommandModeToolbar("command-mode-toolbar"));
@@ -447,4 +453,28 @@ export class OlympusApp {
img.addEventListener("load", () => { SVGInjector(img); });
})
}
#validateData() {
const airbasesValidator = new AirbasesJSONSchemaValidator();
/*
const validator = new Ajv();
const schema = {
type: "object",
properties: {
foo: {type: "integer"},
bar: {type: "string"},
},
required: ["foo"],
additionalProperties: false,
}
const data = this.#getRunwayData();
const validate = validator.compile(schema);
const valid = validate(data);
if (!valid) console.log(validate.errors);
//*/
}
}

View File

@@ -0,0 +1,67 @@
{
"type": "object",
"additionalProperties": false,
"properties": {
"airfields": {
"type": "object",
"minProperties": 1,
"patternProperties": {
".*": {
"type": "object",
"properties": {
"elevation": {
"type": "string",
"pattern": "^(0|([1-9][0-9]*))?$"
},
"ICAO": {
"type": "string"
},
"runways": {
"type": "array",
"items": {
"type": "object",
"additionalProperties": false,
"properties": {
"headings": {
"type": "array",
"items": {
"type": "object",
"patternProperties": {
".*": {
"type": "object",
"properties": {
"ILS": {
"type": "string",
"pattern": "^(1[0-9]{1,2}\\.[0-9][05])?$"
},
"magHeading": {
"type": "string",
"pattern": "^([0-2][0-9]{2})|(3(([0-5][0-9])|(60)))?$"
}
},
"required": ["magHeading"]
}
}
},
"minItems": 1
},
"length": {
"type": "string",
"pattern": "^[1-9][0-9]{3,4}$"
}
},
"required": [ "headings", "length" ]
}
},
"TACAN": {
"type": "string",
"pattern": "^([1-9][0-9]{1,2}X)?$"
}
},
"required": [ "elevation", "runways" ]
}
}
}
},
"required": ["airfields"]
}

View File

@@ -0,0 +1,425 @@
{
"$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

@@ -0,0 +1,82 @@
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() {
const schema = require("../schemas/airbases.schema.json");
super( schema );
[
require( "../../public/databases/airbases/caucasus.json" ),
require( "../../public/databases/airbases/falklands.json" ),
require( "../../public/databases/airbases/marianas.json" ),
require( "../../public/databases/airbases/nevada.json" ),
require( "../../public/databases/airbases/normandy.json" ),
require( "../../public/databases/airbases/persiangulf.json" ),
require( "../../public/databases/airbases/sinaimap.json" ),
require( "../../public/databases/airbases/syria.json" ),
require( "../../public/databases/airbases/thechannel.json" )
].forEach( data => {
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,65 @@ 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) => {
let errorString = error.instancePath.substring(1) + ": " + error.message;
if (error.params) {
const {allowedValues} = error.params;
if (allowedValues)
errorString += ": " + allowedValues.join(', ');
}
acc.push(errorString);
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 +132,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>