305 lines
13 KiB
JavaScript
305 lines
13 KiB
JavaScript
// Rôle : centralise les règles de validation des JSON éditables du projet.
|
|
import assert from "node:assert/strict";
|
|
|
|
const SNAKE_CASE_RE = /^[a-z0-9]+(?:_[a-z0-9]+)*$/;
|
|
const PHYSICAL_HITZONE_KEYS = ["cut", "blunt", "ammo"];
|
|
const ELEMENTAL_HITZONE_KEYS = ["fire", "water", "thunder", "ice", "dragon"];
|
|
|
|
function valueAt(object, path) {
|
|
return path.split(".").reduce((value, key) => value?.[key], object);
|
|
}
|
|
|
|
function assertNonEmptyString(object, path) {
|
|
const value = valueAt(object, path);
|
|
assert.equal(typeof value, "string", `${path} must be a string`);
|
|
assert.ok(value.trim(), `${path} must not be empty`);
|
|
}
|
|
|
|
export function assertStringArray(object, path, minLength = 1) {
|
|
const value = valueAt(object, path);
|
|
assert.ok(Array.isArray(value), `${path} must be an array`);
|
|
assert.ok(value.length >= minLength, `${path} must contain at least ${minLength} item(s)`);
|
|
value.forEach((item, index) => {
|
|
assert.equal(typeof item, "string", `${path}[${index}] must be a string`);
|
|
assert.ok(item.trim(), `${path}[${index}] must not be empty`);
|
|
});
|
|
}
|
|
|
|
export function validateSiteContent(site) {
|
|
[
|
|
"brand.name",
|
|
"brand.homeAriaLabel",
|
|
"navigation.home",
|
|
"navigation.toolboxes",
|
|
"navigation.games",
|
|
"navigation.about",
|
|
"navigation.mobileGames",
|
|
"sidebar.badge",
|
|
"sidebar.note",
|
|
"topbar.dashboard",
|
|
"topbar.toolbox",
|
|
"topbar.games",
|
|
"gamesPage.eyebrow",
|
|
"gamesPage.title",
|
|
"gamesPage.description",
|
|
"gamesPage.emptyTitle",
|
|
"gamesPage.emptyText",
|
|
"home.hero.eyebrow",
|
|
"home.hero.title",
|
|
"home.hero.description",
|
|
"home.hero.primaryAction",
|
|
"home.hero.secondaryAction",
|
|
"home.stats.toolboxSingular",
|
|
"home.stats.toolboxPlural",
|
|
"home.stats.toolSingular",
|
|
"home.stats.toolPlural",
|
|
"home.origin.eyebrow",
|
|
"home.origin.title",
|
|
"home.origin.visualAlt",
|
|
"home.origin.dialogueAriaLabel",
|
|
"home.origin.caption",
|
|
"home.aboutLink",
|
|
"home.legal.copyright",
|
|
"home.legal.disclaimer",
|
|
"about.eyebrow",
|
|
"about.title",
|
|
"about.description",
|
|
"about.toolsTitle",
|
|
"about.limitsTitle",
|
|
"about.contribute.eyebrow",
|
|
"about.contribute.title",
|
|
"about.contribute.description",
|
|
"about.contribute.example",
|
|
"about.contribute.promptTitle",
|
|
"about.contribute.promptIntro",
|
|
"about.contribute.promptText",
|
|
"toolboxes.eyebrow",
|
|
"toolboxes.title",
|
|
"toolboxes.newButton",
|
|
"toolboxes.importAll",
|
|
"toolboxes.exportAll",
|
|
"toolboxes.importOne",
|
|
"toolboxes.summary",
|
|
"toolboxes.storageHelp.title",
|
|
"toolboxes.storageHelp.text",
|
|
"toolboxes.modules.notepad.placeholder",
|
|
"toolboxes.modules.checklist.itemPlaceholder",
|
|
"toolboxes.modules.checklist.sectionPlaceholder",
|
|
"toolboxes.modules.checklist.quantityLabel",
|
|
"toolboxes.modules.checklist.addButton",
|
|
"toolboxes.modules.checklist.importPlaceholder",
|
|
"toolboxes.modules.checklist.importOpenButton",
|
|
"toolboxes.modules.checklist.importModalTitle",
|
|
"toolboxes.modules.checklist.importButton",
|
|
"toolboxes.modules.checklist.hideCompletedSectionsTitle",
|
|
"toolboxes.modules.checklist.hideCompletedSectionsFullyTitle",
|
|
"toolboxes.modules.checklist.completedSectionsModeTitle",
|
|
"toolboxes.modules.checklist.completedSectionsVisibleLabel",
|
|
"toolboxes.modules.checklist.completedSectionsReducedLabel",
|
|
"toolboxes.modules.checklist.completedSectionsHiddenLabel",
|
|
"toolboxes.modules.checklist.showCompletedSectionTitle",
|
|
"toolboxes.modules.checklist.hideCompletedSectionTitle",
|
|
"toolboxes.modules.checklist.decrementLabel",
|
|
"toolboxes.modules.checklist.incrementLabel",
|
|
"toolboxes.modules.checklist.currentQuantityLabel",
|
|
"toolboxes.modules.checklist.deleteTitle",
|
|
"toolboxes.modules.images.addImages",
|
|
"toolboxes.modules.images.pastePlaceholder",
|
|
"toolboxes.modules.images.pasteAriaLabel",
|
|
"toolboxes.modules.images.imageAlt",
|
|
"toolboxes.modules.images.labelPlaceholder",
|
|
"toolboxes.modules.images.labelAriaLabel",
|
|
"toolboxes.modules.images.previewAriaLabel",
|
|
"toolboxes.modules.images.annotateAriaLabel",
|
|
"toolboxes.modules.images.annotateTitle",
|
|
"toolboxes.modules.images.deleteAriaLabel",
|
|
"toolboxes.modules.images.deleteTitle",
|
|
"toolboxes.modules.links.titlePlaceholder",
|
|
"toolboxes.modules.links.urlPlaceholder",
|
|
"toolboxes.modules.links.addButton",
|
|
"toolboxes.modules.links.importPlaceholder",
|
|
"toolboxes.modules.links.importOpenButton",
|
|
"toolboxes.modules.links.importModalTitle",
|
|
"toolboxes.modules.links.importButton",
|
|
"toolboxes.modules.links.copyTitle",
|
|
"toolboxes.modules.links.copiedTitle",
|
|
"toolboxes.modules.links.deleteTitle",
|
|
"toolboxes.modules.counters.labelPlaceholder",
|
|
"toolboxes.modules.counters.addButton",
|
|
"toolboxes.modules.counters.decrementLabel",
|
|
"toolboxes.modules.counters.incrementLabel",
|
|
"toolboxes.modules.counters.resetTitle",
|
|
"toolboxes.modules.counters.deleteTitle",
|
|
"toolboxes.modules.calculator.expressionLabel",
|
|
"toolboxes.modules.calculator.expressionPlaceholder",
|
|
"toolboxes.modules.calculator.resultLabel",
|
|
"toolboxes.modules.calculator.labelLabel",
|
|
"toolboxes.modules.calculator.labelPlaceholder",
|
|
"toolboxes.modules.calculator.saveButton",
|
|
"toolboxes.modules.calculator.rootButton",
|
|
"toolboxes.modules.calculator.resetTitle",
|
|
"toolboxes.modules.calculator.copyTitle",
|
|
"toolboxes.modules.calculator.copiedTitle",
|
|
"toolboxes.modules.calculator.scrollableTitle",
|
|
"toolboxes.modules.calculator.emptyResults",
|
|
"toolboxes.modules.calculator.readonlyValueTitle",
|
|
"toolboxes.modules.calculator.useEntryTitle",
|
|
"toolboxes.modules.calculator.renameTitle",
|
|
"toolboxes.modules.calculator.deleteTitle",
|
|
"toolboxes.modules.imageAnnotation.addImage",
|
|
"toolboxes.modules.imageAnnotation.replaceImage",
|
|
"toolboxes.modules.imageAnnotation.pastePlaceholder",
|
|
"toolboxes.modules.imageAnnotation.pasteAriaLabel",
|
|
"toolboxes.modules.imageAnnotation.stageAriaLabel",
|
|
"toolboxes.modules.imageAnnotation.addMarkerAriaLabel",
|
|
"toolboxes.modules.imageAnnotation.imageAlt",
|
|
"toolboxes.modules.imageAnnotation.previewTitle",
|
|
"toolboxes.modules.imageAnnotation.previewAriaLabel",
|
|
"toolboxes.modules.imageAnnotation.markerPrefix",
|
|
"toolboxes.modules.imageAnnotation.markersSingular",
|
|
"toolboxes.modules.imageAnnotation.markersPlural",
|
|
"toolboxes.modules.imageAnnotation.markerPlaceholder",
|
|
"toolboxes.modules.imageAnnotation.emptyImage",
|
|
"toolboxes.modules.imageAnnotation.emptyMarkers",
|
|
"toolboxes.modules.imageAnnotation.deleteImageTitle",
|
|
"toolboxes.modules.imageAnnotation.deleteMarkerTitle",
|
|
"toolboxes.emptyTitle",
|
|
"toolboxes.emptyText"
|
|
].forEach((path) => assertNonEmptyString(site, path));
|
|
|
|
assertStringArray(site, "toolboxes.storageHelp.items", 3);
|
|
|
|
const aboutLimits = valueAt(site, "about.limits");
|
|
assert.ok(Array.isArray(aboutLimits), "about.limits must be an array");
|
|
assert.ok(aboutLimits.length >= 3, "about.limits must contain at least 3 items");
|
|
aboutLimits.forEach((limit, index) => {
|
|
if (typeof limit === "string") {
|
|
assert.ok(limit.trim(), `about.limits[${index}] must not be empty`);
|
|
return;
|
|
}
|
|
assert.equal(typeof limit, "object", `about.limits[${index}] must be a string or object`);
|
|
assertNonEmptyString({ limit }, "limit.icon");
|
|
assertNonEmptyString({ limit }, "limit.title");
|
|
assertNonEmptyString({ limit }, "limit.text");
|
|
});
|
|
|
|
const aboutSections = valueAt(site, "about.sections");
|
|
assert.ok(Array.isArray(aboutSections), "about.sections must be an array");
|
|
assert.ok(aboutSections.length >= 2, "about.sections must contain at least 2 items");
|
|
aboutSections.forEach((section) => {
|
|
assertNonEmptyString({ section }, "section.title");
|
|
assertNonEmptyString({ section }, "section.text");
|
|
});
|
|
|
|
const aboutTools = valueAt(site, "about.tools");
|
|
assert.ok(Array.isArray(aboutTools), "about.tools must be an array");
|
|
assert.ok(aboutTools.length >= 1, "about.tools must contain at least 1 item");
|
|
aboutTools.forEach((tool) => {
|
|
assertNonEmptyString({ tool }, "tool.icon");
|
|
assertNonEmptyString({ tool }, "tool.name");
|
|
assertNonEmptyString({ tool }, "tool.description");
|
|
});
|
|
|
|
const lines = valueAt(site, "home.origin.lines");
|
|
assert.ok(Array.isArray(lines), "home.origin.lines must be an array");
|
|
assert.ok(lines.length >= 2, "home.origin.lines must contain at least 2 items");
|
|
lines.forEach((line, index) => {
|
|
assert.ok(["user", "app"].includes(line?.speaker), `home.origin.lines[${index}].speaker must be "user" or "app"`);
|
|
assert.equal(typeof line?.text, "string", `home.origin.lines[${index}].text must be a string`);
|
|
assert.ok(line.text.trim(), `home.origin.lines[${index}].text must not be empty`);
|
|
});
|
|
}
|
|
|
|
function assertSnakeCase(value, path) {
|
|
assert.equal(typeof value, "string", `${path} must be a string`);
|
|
assert.match(value, SNAKE_CASE_RE, `${path} must be snake_case`);
|
|
}
|
|
|
|
function validateConditionValues(items, path) {
|
|
assert.ok(Array.isArray(items), `${path} must be an array`);
|
|
items.forEach((item, index) => {
|
|
assertSnakeCase(item?.condition, `${path}[${index}].condition`);
|
|
assert.ok(Array.isArray(item?.values), `${path}[${index}].values must be an array`);
|
|
item.values.forEach((value, valueIndex) => assertSnakeCase(value, `${path}[${index}].values[${valueIndex}]`));
|
|
});
|
|
}
|
|
|
|
export function validateMonsterData(monsters) {
|
|
assert.ok(Array.isArray(monsters), "monsters must be an array");
|
|
const ids = new Set();
|
|
const names = new Set();
|
|
|
|
monsters.forEach((monster, monsterIndex) => {
|
|
const path = `monsters[${monsterIndex}]`;
|
|
assert.equal(typeof monster.id, "number", `${path}.id must be a number`);
|
|
assert.ok(Number.isInteger(monster.id), `${path}.id must be an integer`);
|
|
assert.ok(!ids.has(monster.id), `${path}.id must be unique`);
|
|
ids.add(monster.id);
|
|
|
|
assertSnakeCase(monster.name, `${path}.name`);
|
|
assert.ok(!names.has(monster.name), `${path}.name must be unique`);
|
|
names.add(monster.name);
|
|
assertSnakeCase(monster.type, `${path}.type`);
|
|
validateConditionValues(monster.ailments, `${path}.ailments`);
|
|
validateConditionValues(monster.weaknesses, `${path}.weaknesses`);
|
|
validateConditionValues(monster.weakpoints, `${path}.weakpoints`);
|
|
|
|
assert.equal(monster.damage, undefined, `${path}.damage must not be used`);
|
|
assert.ok(Array.isArray(monster.hitzones), `${path}.hitzones must be an array`);
|
|
assert.ok(monster.hitzones.length > 0, `${path}.hitzones must not be empty`);
|
|
monster.hitzones.forEach((hitzone, hitzoneIndex) => {
|
|
const hitzonePath = `${path}.hitzones[${hitzoneIndex}]`;
|
|
assertSnakeCase(hitzone.name, `${hitzonePath}.name`);
|
|
assert.equal(typeof hitzone.physical, "object", `${hitzonePath}.physical must be an object`);
|
|
assert.equal(typeof hitzone.elemental, "object", `${hitzonePath}.elemental must be an object`);
|
|
PHYSICAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.physical[key], "number", `${hitzonePath}.physical.${key} must be a number`));
|
|
ELEMENTAL_HITZONE_KEYS.forEach((key) => assert.equal(typeof hitzone.elemental[key], "number", `${hitzonePath}.elemental.${key} must be a number`));
|
|
});
|
|
});
|
|
}
|
|
|
|
export function validateEndemicData(endemicPayload) {
|
|
["endemicLife", "aquaticLife"].forEach((collection) => {
|
|
assert.ok(Array.isArray(endemicPayload[collection]), `${collection} must be an array`);
|
|
endemicPayload[collection].forEach((item, itemIndex) => {
|
|
const path = `${collection}[${itemIndex}]`;
|
|
assertSnakeCase(item.name, `${path}.name`);
|
|
if (item.description) assertSnakeCase(item.description, `${path}.description`);
|
|
validateConditionValues(item.locations, `${path}.locations`);
|
|
});
|
|
});
|
|
}
|
|
|
|
export function validateTranslationKeys(translations, path) {
|
|
Object.keys(translations).forEach((key) => {
|
|
assert.ok(!/\s/.test(key), `${path}.${key} must not contain spaces`);
|
|
});
|
|
}
|
|
|
|
export function validateDiablo4Affixes(payload) {
|
|
assert.ok(Array.isArray(payload.categories), "diablo4.categories must be an array");
|
|
assert.ok(payload.categories.length > 0, "diablo4.categories must not be empty");
|
|
assert.ok(Array.isArray(payload.affixes), "diablo4.affixes must be an array");
|
|
assert.ok(payload.affixes.length > 0, "diablo4.affixes must not be empty");
|
|
|
|
const ids = new Set();
|
|
const categoryIds = payload.categories.map((category, index) => {
|
|
assertSnakeCase(category.id, `diablo4.categories[${index}].id`);
|
|
assertNonEmptyString({ category }, "category.label");
|
|
assertSnakeCase(category.icon, `diablo4.categories[${index}].icon`);
|
|
assertSnakeCase(category.tone, `diablo4.categories[${index}].tone`);
|
|
return category.id;
|
|
});
|
|
|
|
payload.affixes.forEach((affix, index) => {
|
|
assertSnakeCase(affix.id, `diablo4.affixes[${index}].id`);
|
|
assert.ok(!ids.has(affix.id), `diablo4.affixes[${index}].id must be unique`);
|
|
ids.add(affix.id);
|
|
assertNonEmptyString({ affix }, "affix.label");
|
|
assert.ok(Array.isArray(affix.categories), `diablo4.affixes[${index}].categories must be an array`);
|
|
affix.categories.forEach((category, categoryIndex) => {
|
|
assertSnakeCase(category, `diablo4.affixes[${index}].categories[${categoryIndex}]`);
|
|
assert.ok(categoryIds.includes(category), `diablo4.affixes[${index}].categories[${categoryIndex}] must exist in diablo4.categories`);
|
|
});
|
|
});
|
|
}
|