// 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.library", "navigation.about", "navigation.mobileLibrary", "navigation.mobileGames", "sidebar.badge", "sidebar.note", "topbar.dashboard", "topbar.toolbox", "topbar.library", "topbar.games", "gamesPage.eyebrow", "gamesPage.title", "gamesPage.description", "gamesPage.emptyTitle", "gamesPage.emptyText", "library.eyebrow", "library.title", "library.description", "library.summaryLabel", "library.tocLabel", "library.toolsLabel", "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.toolsLibraryLink", "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.libraryLink", "toolboxes.importAll", "toolboxes.exportAll", "toolboxes.importOne", "toolboxes.preferencesLabel", "toolboxes.toastPositionLabel", "toolboxes.toastPositionLeft", "toolboxes.toastPositionRight", "toolboxes.summary", "toolboxes.storageHelp.title", "toolboxes.storageHelp.text", "toolboxes.modules.notepad.placeholder", "toolboxes.modules.notepad.toolbarLabel", "toolboxes.modules.notepad.textFormatLabel", "toolboxes.modules.notepad.boldTitle", "toolboxes.modules.notepad.italicTitle", "toolboxes.modules.notepad.underlineTitle", "toolboxes.modules.notepad.strikeTitle", "toolboxes.modules.notepad.headingTitle", "toolboxes.modules.notepad.listGroupLabel", "toolboxes.modules.notepad.bulletListTitle", "toolboxes.modules.notepad.numberListTitle", "toolboxes.modules.notepad.colorGroupLabel", "toolboxes.modules.notepad.textColorLabel", "toolboxes.modules.notepad.highlightColorLabel", "toolboxes.modules.notepad.drawingLabel", "toolboxes.modules.notepad.drawTitle", "toolboxes.modules.notepad.eraseTitle", "toolboxes.modules.notepad.temporaryTitle", "toolboxes.modules.notepad.permanentTitle", "toolboxes.modules.notepad.undoDrawingTitle", "toolboxes.modules.notepad.clearDrawingTitle", "toolboxes.modules.notepad.drawingColorLabel", "toolboxes.modules.notepad.drawingWidthLabel", "toolboxes.modules.notepad.temporaryStatus", "toolboxes.modules.notepad.permanentStatus", "toolboxes.modules.notepad.updatedAtPrefix", "toolboxes.modules.notepad.sessionStorageError", "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.reorderTitle", "toolboxes.modules.checklist.reorderSectionTitle", "toolboxes.modules.checklist.sectionEditTitle", "toolboxes.modules.checklist.itemEditLabel", "toolboxes.modules.checklist.categoryEditLabel", "toolboxes.modules.checklist.editTitle", "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.combos.deviceLabel", "toolboxes.modules.combos.keyboardLayoutLabel", "toolboxes.modules.combos.categoryPlaceholder", "toolboxes.modules.combos.targetCategoryLabel", "toolboxes.modules.combos.noCategoryLabel", "toolboxes.modules.combos.comboNameLabel", "toolboxes.modules.combos.comboNamePlaceholder", "toolboxes.modules.combos.defaultComboName", "toolboxes.modules.combos.emptyDraft", "toolboxes.modules.combos.simultaneousModeTitle", "toolboxes.modules.combos.successiveModeLabel", "toolboxes.modules.combos.simultaneousModeLabel", "toolboxes.modules.combos.removeStepTitle", "toolboxes.modules.combos.removeLastInputTitle", "toolboxes.modules.combos.addComboButton", "toolboxes.modules.combos.saveComboButton", "toolboxes.modules.combos.cancelEditTitle", "toolboxes.modules.combos.cancelEditButton", "toolboxes.modules.combos.paletteLabel", "toolboxes.modules.combos.customKeyPlaceholder", "toolboxes.modules.combos.addCustomKeyButton", "toolboxes.modules.combos.emptyCombos", "toolboxes.modules.combos.emptySequence", "toolboxes.modules.combos.showCategoryTitle", "toolboxes.modules.combos.hideCategoryTitle", "toolboxes.modules.combos.reorderCategoryTitle", "toolboxes.modules.combos.reorderComboTitle", "toolboxes.modules.combos.editComboTitle", "toolboxes.modules.combos.deleteComboTitle", "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.table.toolbarLabel", "toolboxes.modules.table.gridLabel", "toolboxes.modules.table.cellLabel", "toolboxes.modules.table.rowActionsLabel", "toolboxes.modules.table.columnActionsLabel", "toolboxes.modules.table.sizeLabel", "toolboxes.modules.table.addRowTitle", "toolboxes.modules.table.removeRowTitle", "toolboxes.modules.table.addColumnTitle", "toolboxes.modules.table.removeColumnTitle", "toolboxes.modules.table.copyTitle", "toolboxes.modules.table.copiedTitle", "toolboxes.modules.table.errorTitle", "toolboxes.modules.table.columnLabel", "toolboxes.modules.table.rowLabel", "toolboxes.modules.table.columnFormulaTitle", "toolboxes.modules.table.rowFormulaTitle", "toolboxes.modules.timer.stopwatchTab", "toolboxes.modules.timer.countdownTab", "toolboxes.modules.timer.stopwatchTitle", "toolboxes.modules.timer.countdownTitle", "toolboxes.modules.timer.startButton", "toolboxes.modules.timer.pauseButton", "toolboxes.modules.timer.lapButton", "toolboxes.modules.timer.resetTitle", "toolboxes.modules.timer.lapLabelAriaLabel", "toolboxes.modules.timer.countdownLabel", "toolboxes.modules.timer.countdownLabelPlaceholder", "toolboxes.modules.timer.countdownTypeLabel", "toolboxes.modules.timer.durationType", "toolboxes.modules.timer.dailyTimeType", "toolboxes.modules.timer.timePatternType", "toolboxes.modules.timer.intervalType", "toolboxes.modules.timer.durationLabel", "toolboxes.modules.timer.dailyTimeLabel", "toolboxes.modules.timer.timePatternLabel", "toolboxes.modules.timer.intervalLabel", "toolboxes.modules.timer.addCountdownButton", "toolboxes.modules.timer.defaultCountdownLabel", "toolboxes.modules.timer.finishedLabel", "toolboxes.modules.timer.targetLabel", "toolboxes.modules.timer.stepLabel", "toolboxes.modules.timer.stepsTitle", "toolboxes.modules.timer.countdownsTitle", "toolboxes.modules.timer.scrollableTitle", "toolboxes.modules.timer.sortTitle", "toolboxes.modules.timer.alertModeTitle", "toolboxes.modules.timer.alertOffTitle", "toolboxes.modules.timer.alertVisibleTitle", "toolboxes.modules.timer.alertSiteTitle", "toolboxes.modules.timer.alertTooSoonTitle", "toolboxes.modules.timer.alertMessage", "toolboxes.modules.timer.emptySteps", "toolboxes.modules.timer.emptyCountdowns", "toolboxes.modules.timer.renameTitle", "toolboxes.modules.timer.deleteTitle", "toolboxes.modules.taskPlanner.settingsTitle", "toolboxes.modules.taskPlanner.settingsButtonLabel", "toolboxes.modules.taskPlanner.hideCompletedTitle", "toolboxes.modules.taskPlanner.globalWeeklyResetDayLabel", "toolboxes.modules.taskPlanner.resetTimeLabel", "toolboxes.modules.taskPlanner.clearCompletedUniqueTitle", "toolboxes.modules.taskPlanner.clearCompletedUniqueButton", "toolboxes.modules.taskPlanner.titlePlaceholder", "toolboxes.modules.taskPlanner.titleLabel", "toolboxes.modules.taskPlanner.descriptionPlaceholder", "toolboxes.modules.taskPlanner.descriptionLabel", "toolboxes.modules.taskPlanner.typeLabel", "toolboxes.modules.taskPlanner.uniqueType", "toolboxes.modules.taskPlanner.dailyType", "toolboxes.modules.taskPlanner.weeklyType", "toolboxes.modules.taskPlanner.addButton", "toolboxes.modules.taskPlanner.reorderTitle", "toolboxes.modules.taskPlanner.checkTitle", "toolboxes.modules.taskPlanner.uncheckTitle", "toolboxes.modules.taskPlanner.deleteTitle", "toolboxes.modules.taskPlanner.showDescriptionTitle", "toolboxes.modules.taskPlanner.hideDescriptionTitle", "toolboxes.modules.taskPlanner.taskSettingsTitle", "toolboxes.modules.taskPlanner.missingPrerequisiteTitle", "toolboxes.modules.taskPlanner.missingPrerequisiteBadge", "toolboxes.modules.taskPlanner.missingPrerequisiteNotice", "toolboxes.modules.taskPlanner.categoryLabel", "toolboxes.modules.taskPlanner.categoryPlaceholder", "toolboxes.modules.taskPlanner.categoryReorderTitle", "toolboxes.modules.taskPlanner.uncategorizedDropTitle", "toolboxes.modules.taskPlanner.showCategoryTitle", "toolboxes.modules.taskPlanner.hideCategoryTitle", "toolboxes.modules.taskPlanner.taskWeeklyResetDayLabel", "toolboxes.modules.taskPlanner.inheritWeeklyResetDay", "toolboxes.modules.taskPlanner.linksLabel", "toolboxes.modules.taskPlanner.emptyLinks", "toolboxes.modules.taskPlanner.addLinkLabel", "toolboxes.modules.taskPlanner.prerequisiteCheckboxLabel", "toolboxes.modules.taskPlanner.removeRelationTitle", "toolboxes.modules.taskPlanner.emptyTasks", "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.modules.imageAnnotation.drawingLabel", "toolboxes.modules.imageAnnotation.drawTitle", "toolboxes.modules.imageAnnotation.eraseTitle", "toolboxes.modules.imageAnnotation.undoDrawingTitle", "toolboxes.modules.imageAnnotation.clearDrawingTitle", "toolboxes.modules.imageAnnotation.drawingColorLabel", "toolboxes.modules.imageAnnotation.drawingWidthLabel", "toolboxes.modules.imageAnnotation.temporaryTitle", "toolboxes.modules.imageAnnotation.permanentTitle", "toolboxes.modules.imageAnnotation.sessionStorageError", "toolboxes.emptyTitle", "toolboxes.emptyText" ].forEach((path) => assertNonEmptyString(site, path)); assertStringArray(site, "toolboxes.storageHelp.items", 3); const librarySummaryItems = valueAt(site, "library.summaryItems"); assert.ok(Array.isArray(librarySummaryItems), "library.summaryItems must be an array"); assert.ok(librarySummaryItems.length >= 3, "library.summaryItems must contain at least 3 items"); librarySummaryItems.forEach((item, index) => { assertNonEmptyString({ item }, `item.icon`); assertNonEmptyString({ item }, `item.title`); assertNonEmptyString({ item }, `item.text`); assert.equal(typeof item, "object", `library.summaryItems[${index}] must be an object`); }); 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`); }); } export function validateLibraryData(payload) { assert.equal(typeof payload?.toolbox, "object", "library.toolbox must be an object"); assert.equal(typeof payload?.modules, "object", "library.modules must be an object"); assertNonEmptyString(payload, "toolbox.name"); assert.ok(Array.isArray(payload.toolbox.modules), "library.toolbox.modules must be an array"); assert.ok(payload.toolbox.modules.length >= 1, "library.toolbox.modules must not be empty"); const moduleIds = new Set(); payload.toolbox.modules.forEach((module, index) => { assertNonEmptyString({ module }, "module.id"); assertNonEmptyString({ module }, "module.type"); assert.ok(!moduleIds.has(module.id), `library.toolbox.modules[${index}].id must be unique`); moduleIds.add(module.id); assert.equal(typeof payload.modules[module.id], "object", `library.modules.${module.id} must be an object`); }); ["notepad", "checklist", "images", "links", "counters", "combos", "calculator", "table", "timer", "taskPlanner", "imageAnnotation"].forEach((type) => { assert.ok(payload.toolbox.modules.some((module) => module.type === type), `library must include a ${type} example`); }); const imagesModule = payload.toolbox.modules.find((module) => module.type === "images"); const imagesData = payload.modules[imagesModule.id]; assert.ok(Array.isArray(imagesData.images), "library images example must contain images"); assert.ok(imagesData.images.length >= 1, "library images example must not be empty"); imagesData.images.forEach((image, index) => { assertNonEmptyString({ image }, "image.id"); assertNonEmptyString({ image }, "image.dataUrl"); assert.match(image.dataUrl, /^data:image\//, `library images[${index}].dataUrl must be an inline image`); }); const taskModule = payload.toolbox.modules.find((module) => module.type === "taskPlanner"); const taskData = payload.modules[taskModule.id]; assert.ok(Array.isArray(taskData.tasks), "library task planner example must contain tasks"); assert.ok(Array.isArray(taskData.relations), "library task planner example must contain relations"); assert.ok(taskData.relations.length >= 1, "library task planner example must include a parent/child relation"); } 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`); }); }); }