import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum'; import { IEntity } from '../infrastructure/Entity/IEntity'; import { ICategory } from './ICategory'; import { IScript } from './IScript'; import { RecommendationLevel } from './RecommendationLevel'; import { OperatingSystem } from './OperatingSystem'; import { IScriptingDefinition } from './IScriptingDefinition'; import { ICategoryCollection } from './ICategoryCollection'; export class CategoryCollection implements ICategoryCollection { public get totalScripts(): number { return this.queryable.allScripts.length; } public get totalCategories(): number { return this.queryable.allCategories.length; } private readonly queryable: IQueryableCollection; constructor( public readonly os: OperatingSystem, public readonly actions: ReadonlyArray, public readonly scripting: IScriptingDefinition) { if (!scripting) { throw new Error('undefined scripting definition'); } this.queryable = makeQueryable(actions); assertInRange(os, OperatingSystem); ensureValid(this.queryable); ensureNoDuplicates(this.queryable.allCategories); ensureNoDuplicates(this.queryable.allScripts); } public findCategory(categoryId: number): ICategory | undefined { return this.queryable.allCategories.find((category) => category.id === categoryId); } public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] { if (isNaN(level)) { throw new Error('undefined level'); } if (!(level in RecommendationLevel)) { throw new Error(`invalid level: ${level}`); } return this.queryable.scriptsByLevel.get(level); } public findScript(scriptId: string): IScript | undefined { return this.queryable.allScripts.find((script) => script.id === scriptId); } public getAllScripts(): IScript[] { return this.queryable.allScripts; } public getAllCategories(): ICategory[] { return this.queryable.allCategories; } } function ensureNoDuplicates(entities: ReadonlyArray>) { const totalOccurrencesById = new Map(); for (const entity of entities) { totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1); } const duplicatedIds = new Array(); totalOccurrencesById.forEach((index, id) => { if (index > 1) { duplicatedIds.push(id); } }); if (duplicatedIds.length > 0) { const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(','); throw new Error( `Duplicate entities are detected with following id(s): ${duplicatedIdsText}`); } } interface IQueryableCollection { allCategories: ICategory[]; allScripts: IScript[]; scriptsByLevel: Map; } function ensureValid(application: IQueryableCollection) { ensureValidCategories(application.allCategories); ensureValidScripts(application.allScripts); } function ensureValidCategories(allCategories: readonly ICategory[]) { if (!allCategories || allCategories.length === 0) { throw new Error('must consist of at least one category'); } } function ensureValidScripts(allScripts: readonly IScript[]) { if (!allScripts || allScripts.length === 0) { throw new Error('must consist of at least one script'); } for (const level of getEnumValues(RecommendationLevel)) { if (allScripts.every((script) => script.level !== level)) { throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`); } } } function flattenApplication(categories: ReadonlyArray): [ICategory[], IScript[]] { const allCategories = new Array(); const allScripts = new Array(); flattenCategories(categories, allCategories, allScripts); return [ allCategories, allScripts, ]; } function flattenCategories( categories: ReadonlyArray, allCategories: ICategory[], allScripts: IScript[]): IQueryableCollection { if (!categories || categories.length === 0) { return; } for (const category of categories) { allCategories.push(category); flattenScripts(category.scripts, allScripts); flattenCategories(category.subCategories, allCategories, allScripts); } } function flattenScripts( scripts: ReadonlyArray, allScripts: IScript[]): IScript[] { if (!scripts) { return; } for (const script of scripts) { allScripts.push(script); } } function makeQueryable( actions: ReadonlyArray): IQueryableCollection { const flattened = flattenApplication(actions); return { allCategories: flattened[0], allScripts: flattened[1], scriptsByLevel: groupByLevel(flattened[1]), }; } function groupByLevel(allScripts: readonly IScript[]): Map { const map = new Map(); for (const levelName of getEnumNames(RecommendationLevel)) { const level = RecommendationLevel[levelName]; const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level); map.set(level, scripts); } return map; }