Initial commit
This commit is contained in:
118
src/domain/Application.ts
Normal file
118
src/domain/Application.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { ICategory } from './ICategory';
|
||||
import { IScript } from './IScript';
|
||||
import { IApplication } from './IApplication';
|
||||
|
||||
export class Application implements IApplication {
|
||||
private static mustHaveCategories(categories: ReadonlyArray<ICategory>) {
|
||||
if (!categories || categories.length === 0) {
|
||||
throw new Error('an application must consist of at least one category');
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Checks all categories against duplicates, throws exception if it find any duplicates
|
||||
* @return {number} Total unique categories
|
||||
*/
|
||||
/** Checks all categories against duplicates, throws exception if it find any duplicates returns total categories */
|
||||
private static mustNotHaveDuplicatedCategories(categories: ReadonlyArray<ICategory>): number {
|
||||
return Application.ensureNoDuplicateEntities(categories, Application.visitAllCategoriesOnce);
|
||||
}
|
||||
/**
|
||||
* Checks all scripts against duplicates, throws exception if it find any scripts duplicates total scripts.
|
||||
* @return {number} Total unique scripts
|
||||
*/
|
||||
private static mustNotHaveDuplicatedScripts(categories: ReadonlyArray<ICategory>): number {
|
||||
return Application.ensureNoDuplicateEntities(categories, Application.visitAllScriptsOnce);
|
||||
}
|
||||
/**
|
||||
* Checks entities against duplicates using a visit function, throws exception if it find any duplicates.
|
||||
* @return {number} Result from the visit function
|
||||
*/
|
||||
private static ensureNoDuplicateEntities<TKey>(
|
||||
categories: ReadonlyArray<ICategory>,
|
||||
visitFunction: (categories: ReadonlyArray<ICategory>,
|
||||
handler: (entity: IEntity<TKey>) => any) => number): number {
|
||||
const totalOccurencesById = new Map<TKey, number>();
|
||||
const totalVisited = visitFunction(categories,
|
||||
(entity) =>
|
||||
totalOccurencesById.set(entity.id,
|
||||
(totalOccurencesById.get(entity.id) || 0) + 1));
|
||||
const duplicatedIds = new Array<TKey>();
|
||||
totalOccurencesById.forEach((count, id) => {
|
||||
if (count > 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}`);
|
||||
}
|
||||
return totalVisited;
|
||||
}
|
||||
// Runs handler on each category and returns sum of total visited categories
|
||||
private static visitAllCategoriesOnce(
|
||||
categories: ReadonlyArray<ICategory>, handler: (category: ICategory) => any): number {
|
||||
let total = 0;
|
||||
for (const category of categories) {
|
||||
handler(category);
|
||||
total++;
|
||||
if (category.subCategories && category.subCategories.length > 0) {
|
||||
total += Application.visitAllCategoriesOnce(
|
||||
category.subCategories as ReadonlyArray<ICategory>, handler);
|
||||
}
|
||||
}
|
||||
return total;
|
||||
}
|
||||
// Runs handler on each script and returns sum of total visited scripts
|
||||
private static visitAllScriptsOnce(
|
||||
categories: ReadonlyArray<ICategory>, handler: (script: IScript) => any): number {
|
||||
let total = 0;
|
||||
Application.visitAllCategoriesOnce(categories, (category) => {
|
||||
if (category.scripts) {
|
||||
for (const script of category.scripts) {
|
||||
handler(script);
|
||||
total++;
|
||||
}
|
||||
}
|
||||
});
|
||||
return total;
|
||||
}
|
||||
public readonly totalScripts: number;
|
||||
public readonly totalCategories: number;
|
||||
|
||||
constructor(
|
||||
public readonly name: string,
|
||||
public readonly version: number,
|
||||
public readonly categories: ReadonlyArray<ICategory>) {
|
||||
Application.mustHaveCategories(categories);
|
||||
this.totalCategories = Application.mustNotHaveDuplicatedCategories(categories);
|
||||
this.totalScripts = Application.mustNotHaveDuplicatedScripts(categories);
|
||||
}
|
||||
|
||||
public findCategory(categoryId: number): ICategory | undefined {
|
||||
let result: ICategory | undefined;
|
||||
Application.visitAllCategoriesOnce(this.categories, (category) => {
|
||||
if (category.id === categoryId) {
|
||||
result = category;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
public findScript(scriptId: string): IScript | undefined {
|
||||
let result: IScript | undefined;
|
||||
Application.visitAllScriptsOnce(this.categories, (script) => {
|
||||
if (script.id === scriptId) {
|
||||
result = script;
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
public getAllScripts(): IScript[] {
|
||||
const result = new Array<IScript>();
|
||||
Application.visitAllScriptsOnce(this.categories, (script) => {
|
||||
result.push(script);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
}
|
||||
25
src/domain/Category.ts
Normal file
25
src/domain/Category.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { BaseEntity } from '../infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from './IScript';
|
||||
import { ICategory } from './ICategory';
|
||||
|
||||
export class Category extends BaseEntity<number> implements ICategory {
|
||||
private static validate(category: ICategory) {
|
||||
if (!category.name) {
|
||||
throw new Error('name is null or empty');
|
||||
}
|
||||
if ((!category.subCategories || category.subCategories.length === 0)
|
||||
&& (!category.scripts || category.scripts.length === 0)) {
|
||||
throw new Error('A category must have at least one sub-category or scripts');
|
||||
}
|
||||
}
|
||||
|
||||
constructor(
|
||||
id: number,
|
||||
public readonly name: string,
|
||||
public readonly documentationUrls: ReadonlyArray<string>,
|
||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
||||
public readonly scripts?: ReadonlyArray<IScript>) {
|
||||
super(id);
|
||||
Category.validate(this);
|
||||
}
|
||||
}
|
||||
12
src/domain/IApplication.ts
Normal file
12
src/domain/IApplication.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IScript } from '@/domain/IScript';
|
||||
import { ICategory } from '@/domain/ICategory';
|
||||
|
||||
export interface IApplication {
|
||||
readonly categories: ReadonlyArray<ICategory>;
|
||||
findCategory(categoryId: number): ICategory | undefined;
|
||||
findScript(scriptId: string): IScript | undefined;
|
||||
getAllScripts(): ReadonlyArray<IScript>;
|
||||
}
|
||||
|
||||
export { IScript } from '@/domain/IScript';
|
||||
export { ICategory } from '@/domain/ICategory';
|
||||
13
src/domain/ICategory.ts
Normal file
13
src/domain/ICategory.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
import { IScript } from './IScript';
|
||||
import { IDocumentable } from './IDocumentable';
|
||||
|
||||
export interface ICategory extends IEntity<number>, IDocumentable {
|
||||
readonly id: number;
|
||||
readonly name: string;
|
||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
||||
readonly scripts?: ReadonlyArray<IScript>;
|
||||
}
|
||||
|
||||
export { IEntity } from '../infrastructure/Entity/IEntity';
|
||||
export { IScript } from './IScript';
|
||||
3
src/domain/IDocumentable.ts
Normal file
3
src/domain/IDocumentable.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
export interface IDocumentable {
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
}
|
||||
8
src/domain/IScript.ts
Normal file
8
src/domain/IScript.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { IEntity } from './../infrastructure/Entity/IEntity';
|
||||
import { IDocumentable } from './IDocumentable';
|
||||
|
||||
export interface IScript extends IEntity<string>, IDocumentable {
|
||||
readonly name: string;
|
||||
readonly code: string;
|
||||
readonly documentationUrls: ReadonlyArray<string>;
|
||||
}
|
||||
51
src/domain/Script.ts
Normal file
51
src/domain/Script.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import { IScript } from './IScript';
|
||||
|
||||
export class Script extends BaseEntity<string> implements IScript {
|
||||
private static ensureNoEmptyLines(name: string, code: string): void {
|
||||
if (code.split('\n').some((line) => line.trim().length === 0)) {
|
||||
throw Error(`Script has empty lines "${name}"`);
|
||||
}
|
||||
}
|
||||
|
||||
private static ensureCodeHasUniqueLines(name: string, code: string): void {
|
||||
const lines = code.split('\n');
|
||||
if (lines.length === 0) {
|
||||
return;
|
||||
}
|
||||
const checkForDuplicates = (line: string) => {
|
||||
const trimmed = line.trim();
|
||||
if (trimmed.length === 1 && trimmed === ')' || trimmed === '(') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
const duplicateLines = new Array<string>();
|
||||
const uniqueLines = new Set<string>();
|
||||
let validatedLineCount = 0;
|
||||
for (const line of lines) {
|
||||
if (!checkForDuplicates(line)) {
|
||||
continue;
|
||||
}
|
||||
uniqueLines.add(line);
|
||||
if (uniqueLines.size !== validatedLineCount + 1) {
|
||||
duplicateLines.push(line);
|
||||
}
|
||||
validatedLineCount++;
|
||||
}
|
||||
if (duplicateLines.length !== 0) {
|
||||
throw Error(`Duplicates detected in script "${name}":\n ${duplicateLines.join('\n')}`);
|
||||
}
|
||||
}
|
||||
|
||||
constructor(public name: string, public code: string, public documentationUrls: ReadonlyArray<string>) {
|
||||
super(name);
|
||||
if (code == null || code.length === 0) {
|
||||
throw new Error('Code is empty or null');
|
||||
}
|
||||
Script.ensureCodeHasUniqueLines(name, code);
|
||||
Script.ensureNoEmptyLines(name, code);
|
||||
}
|
||||
}
|
||||
|
||||
export { IScript } from './IScript';
|
||||
Reference in New Issue
Block a user