Refactor executable IDs to use strings #262
This commit unifies executable ID structure across categories and scripts, paving the way for more complex ID solutions for #262. It also refactors related code to adapt to the changes. Key changes: - Change numeric IDs to string IDs for categories - Use named types for string IDs to improve code clarity - Add unit tests to verify ID uniqueness Other supporting changes: - Separate concerns in entities for data access and executables by using separate abstractions (`Identifiable` and `RepositoryEntity`) - Simplify usage and construction of entities. - Remove `BaseEntity` for simplicity. - Move creation of categories/scripts to domain layer - Refactor CategoryCollection for better validation logic isolation - Rename some categories to keep the names (used as pseudo-IDs) unique on Windows.
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import type { IApplication } from '@/domain/IApplication';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { ProjectDetailsStub } from './ProjectDetailsStub';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||
import type { CategoryCollectionInitParameters } from '@/domain/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { CategoryCollectionInitParameters } from '@/domain/Collection/CategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||
|
||||
export function createCategoryCollectionFactorySpy(): {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { getEnumValues } from '@/application/Common/Enum';
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { ICategoryCollectionState } from '@/application/Context/State/ICate
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import type { UserSelection } from '@/application/Context/State/Selection/UserSelection';
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import type { FilterContext } from '@/application/Context/State/Filter/FilterContext';
|
||||
@@ -33,7 +33,10 @@ export class CategoryCollectionStateStub implements ICategoryCollectionState {
|
||||
this.collection = new CategoryCollectionStub()
|
||||
.withOs(this.os)
|
||||
.withTotalScripts(this.allScripts.length)
|
||||
.withAction(new CategoryStub(0).withScripts(...allScripts));
|
||||
.withAction(
|
||||
new CategoryStub(`[${CategoryCollectionStateStub.name}]-default-action`)
|
||||
.withScripts(...allScripts),
|
||||
);
|
||||
}
|
||||
|
||||
public withCollection(collection: ICategoryCollection): this {
|
||||
|
||||
@@ -2,8 +2,9 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
import { ScriptingDefinitionStub } from './ScriptingDefinitionStub';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
@@ -22,9 +23,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
public readonly actions = new Array<Category>();
|
||||
|
||||
public withSomeActions(): this {
|
||||
this.withAction(new CategoryStub(1));
|
||||
this.withAction(new CategoryStub(2));
|
||||
this.withAction(new CategoryStub(3));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-1`));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-2`));
|
||||
this.withAction(new CategoryStub(`[${CategoryCollectionStub}]-action-3`));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -60,9 +61,9 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
return this;
|
||||
}
|
||||
|
||||
public getCategory(categoryId: number): Category {
|
||||
public getCategory(categoryId: ExecutableId): Category {
|
||||
return this.getAllCategories()
|
||||
.find((category) => category.id === categoryId)
|
||||
.find((category) => category.executableId === categoryId)
|
||||
?? new CategoryStub(categoryId);
|
||||
}
|
||||
|
||||
@@ -71,10 +72,10 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
.filter((script) => script.level !== undefined && script.level <= level);
|
||||
}
|
||||
|
||||
public getScript(scriptId: string): Script {
|
||||
public getScript(executableId: ExecutableId): Script {
|
||||
return this.getAllScripts()
|
||||
.find((script) => scriptId === script.id)
|
||||
?? new ScriptStub(scriptId);
|
||||
.find((script) => executableId === script.executableId)
|
||||
?? new ScriptStub(executableId);
|
||||
}
|
||||
|
||||
public getAllScripts(): ReadonlyArray<Script> {
|
||||
@@ -89,7 +90,7 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
||||
}
|
||||
|
||||
function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category> {
|
||||
return (category.subCategories || []).flatMap(
|
||||
return (category.subcategories || []).flatMap(
|
||||
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
|
||||
);
|
||||
}
|
||||
@@ -97,7 +98,7 @@ function getSubCategoriesRecursively(category: Category): ReadonlyArray<Category
|
||||
function getScriptsRecursively(category: Category): ReadonlyArray<Script> {
|
||||
return [
|
||||
...(category.scripts || []),
|
||||
...(category.subCategories || []).flatMap(
|
||||
...(category.subcategories || []).flatMap(
|
||||
(subCategory) => getScriptsRecursively(subCategory),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import type { CategoryCollectionValidationContext } from '@/domain/Collection/Validation/CategoryCollectionValidator';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class CategoryCollectionValidationContextStub
|
||||
implements CategoryCollectionValidationContext {
|
||||
public allScripts: readonly Script[] = [
|
||||
new ScriptStub(`[${CategoryCollectionValidationContextStub.name}] test-script`),
|
||||
];
|
||||
|
||||
public allCategories: readonly Category[] = [
|
||||
new CategoryStub(`[${CategoryCollectionValidationContextStub.name}] test-category`),
|
||||
];
|
||||
|
||||
public operatingSystem: OperatingSystem = OperatingSystem.iPadOS;
|
||||
|
||||
public withOperatingSystem(operatingSystem: OperatingSystem): this {
|
||||
this.operatingSystem = operatingSystem;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAllCategories(allCategories: readonly Category[]): this {
|
||||
this.allCategories = allCategories;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withAllScripts(allScripts: readonly Script[]): this {
|
||||
this.allScripts = allScripts;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { CategoryFactory } from '@/application/Parser/Executable/CategoryParser';
|
||||
import type { CategoryInitParameters } from '@/domain/Executables/Category/CollectionCategory';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import type { CategoryFactory, CategoryInitParameters } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { CategoryStub } from './CategoryStub';
|
||||
|
||||
export function createCategoryFactorySpy(): {
|
||||
@@ -10,7 +9,7 @@ export function createCategoryFactorySpy(): {
|
||||
const createdCategories = new Map<Category, CategoryInitParameters>();
|
||||
return {
|
||||
categoryFactorySpy: (parameters) => {
|
||||
const category = new CategoryStub(55);
|
||||
const category = new CategoryStub('category-from-factory-stub');
|
||||
createdCategories.set(category, parameters);
|
||||
return category;
|
||||
},
|
||||
|
||||
@@ -16,7 +16,7 @@ export class CategoryParserStub {
|
||||
if (result) {
|
||||
return result;
|
||||
}
|
||||
return new CategoryStub(5489);
|
||||
return new CategoryStub(`[${CategoryParserStub.name}]-parsed-category`);
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import type { CategorySelection } from '@/application/Context/State/Selection/Category/CategorySelection';
|
||||
import type { CategorySelectionChangeCommand } from '@/application/Context/State/Selection/Category/CategorySelectionChange';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class CategorySelectionStub
|
||||
extends StubWithObservableMethodCalls<CategorySelection>
|
||||
implements CategorySelection {
|
||||
public isCategorySelected(categoryId: number, revert: boolean): boolean {
|
||||
public isCategorySelected(categoryId: ExecutableId, revert: boolean): boolean {
|
||||
const call = this.callHistory.find(
|
||||
(c) => c.methodName === 'processChanges'
|
||||
&& c.args[0].changes.some((change) => (
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Category } from '@/domain/Executables/Category/Category';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
public name = `category-with-id-${this.id}`;
|
||||
export class CategoryStub implements Category {
|
||||
public name = `[${CategoryStub.name}] name (ID: ${this.executableId})`;
|
||||
|
||||
public readonly subCategories = new Array<Category>();
|
||||
public readonly subcategories = new Array<Category>();
|
||||
|
||||
public readonly scripts = new Array<Script>();
|
||||
|
||||
@@ -15,25 +15,25 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
|
||||
private allScriptsRecursively: (readonly Script[]) | undefined;
|
||||
|
||||
public constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
public constructor(
|
||||
readonly executableId: ExecutableId,
|
||||
) { }
|
||||
|
||||
public includes(script: Script): boolean {
|
||||
return this.getAllScriptsRecursively().some((s) => s.id === script.id);
|
||||
return this.getAllScriptsRecursively().some((s) => s.executableId === script.executableId);
|
||||
}
|
||||
|
||||
public getAllScriptsRecursively(): readonly Script[] {
|
||||
if (this.allScriptsRecursively === undefined) {
|
||||
return [
|
||||
...this.scripts,
|
||||
...this.subCategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
...this.subcategories.flatMap((c) => c.getAllScriptsRecursively()),
|
||||
];
|
||||
}
|
||||
return this.allScriptsRecursively;
|
||||
}
|
||||
|
||||
public withScriptIds(...scriptIds: readonly string[]): this {
|
||||
public withScriptIds(...scriptIds: readonly ExecutableId[]): this {
|
||||
return this.withScripts(
|
||||
...scriptIds.map((id) => new ScriptStub(id)),
|
||||
);
|
||||
@@ -70,7 +70,7 @@ export class CategoryStub extends BaseEntity<number> implements Category {
|
||||
}
|
||||
|
||||
public withCategory(category: Category): this {
|
||||
this.subCategories.push(category);
|
||||
this.subcategories.push(category);
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,8 +19,13 @@ export class FilterResultStub implements FilterResult {
|
||||
|
||||
public withSomeMatches() {
|
||||
return this
|
||||
.withCategoryMatches([new CategoryStub(3).withScriptIds('script-1')])
|
||||
.withScriptMatches([new ScriptStub('script-2')]);
|
||||
.withCategoryMatches([
|
||||
new CategoryStub(`[${FilterResultStub.name}]-matched-category-1`)
|
||||
.withScriptIds(`[${FilterResultStub.name}]-matched-script-1`),
|
||||
])
|
||||
.withScriptMatches([
|
||||
new ScriptStub(`[${FilterResultStub.name}]-matched-script-2`),
|
||||
]);
|
||||
}
|
||||
|
||||
public withCategoryMatches(matches: readonly Category[]) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { FilterResult } from '@/application/Context/State/Filter/Result/FilterResult';
|
||||
import type { FilterStrategy } from '@/application/Context/State/Filter/Strategy/FilterStrategy';
|
||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
||||
import { FilterResultStub } from './FilterResultStub';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { type NodeMetadata, NodeType } from '@/presentation/components/Scripts/View/Tree/NodeContent/NodeMetadata';
|
||||
|
||||
export class NodeMetadataStub implements NodeMetadata {
|
||||
public id = 'stub-id';
|
||||
public executableId: ExecutableId = 'stub-id';
|
||||
|
||||
public readonly text: string = 'stub-text';
|
||||
|
||||
@@ -18,8 +19,8 @@ export class NodeMetadataStub implements NodeMetadata {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
this.id = id;
|
||||
public withId(executableId: ExecutableId): this {
|
||||
this.executableId = executableId;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
|
||||
export class NumericEntityStub extends BaseEntity<number> {
|
||||
public customProperty = 'customProperty';
|
||||
|
||||
public constructor(id: number) {
|
||||
super(id);
|
||||
}
|
||||
|
||||
public withCustomProperty(value: string): NumericEntityStub {
|
||||
this.customProperty = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
14
tests/unit/shared/Stubs/RepositoryEntityStub.ts
Normal file
14
tests/unit/shared/Stubs/RepositoryEntityStub.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { RepositoryEntity, RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
|
||||
|
||||
export class RepositoryEntityStub implements RepositoryEntity {
|
||||
public customProperty = 'customProperty';
|
||||
|
||||
public constructor(
|
||||
public readonly id: RepositoryEntityId,
|
||||
) { }
|
||||
|
||||
public withCustomPropertyValue(value: string): RepositoryEntityStub {
|
||||
this.customProperty = value;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ScriptInitParameters } from '@/domain/Executables/Script/CollectionScript';
|
||||
import type { ScriptFactory, ScriptInitParameters } from '@/domain/Executables/Script/ScriptFactory';
|
||||
import { ScriptStub } from './ScriptStub';
|
||||
|
||||
export function createScriptFactorySpy(): {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { SelectedScript } from '@/application/Context/State/Selection/Scrip
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from '@/application/Context/State/Selection/Script/ScriptSelectionChange';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
import { EventSourceStub } from './EventSourceStub';
|
||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||
@@ -32,9 +33,9 @@ export class ScriptSelectionStub
|
||||
return this;
|
||||
}
|
||||
|
||||
public isScriptSelected(scriptId: string, revert: boolean): boolean {
|
||||
public isScriptSelected(scriptExecutableId: ExecutableId, revert: boolean): boolean {
|
||||
return this.isScriptChanged({
|
||||
scriptId,
|
||||
scriptId: scriptExecutableId,
|
||||
newStatus: {
|
||||
isSelected: true,
|
||||
isReverted: revert,
|
||||
@@ -42,9 +43,9 @@ export class ScriptSelectionStub
|
||||
});
|
||||
}
|
||||
|
||||
public isScriptDeselected(scriptId: string): boolean {
|
||||
public isScriptDeselected(scriptExecutableId: ExecutableId): boolean {
|
||||
return this.isScriptChanged({
|
||||
scriptId,
|
||||
scriptId: scriptExecutableId,
|
||||
newStatus: {
|
||||
isSelected: false,
|
||||
},
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import { SelectedScriptStub } from './SelectedScriptStub';
|
||||
|
||||
export class ScriptStub extends BaseEntity<string> implements Script {
|
||||
public name = `name${this.id}`;
|
||||
export class ScriptStub implements Script {
|
||||
public name = `name${this.executableId}`;
|
||||
|
||||
public code: ScriptCode = {
|
||||
execute: `REM execute-code (${this.id})`,
|
||||
revert: `REM revert-code (${this.id})`,
|
||||
execute: `REM execute-code (${this.executableId})`,
|
||||
revert: `REM revert-code (${this.executableId})`,
|
||||
};
|
||||
|
||||
public docs: readonly string[] = new Array<string>();
|
||||
@@ -18,9 +18,7 @@ export class ScriptStub extends BaseEntity<string> implements Script {
|
||||
|
||||
private isReversible: boolean | undefined = undefined;
|
||||
|
||||
constructor(public readonly id: string) {
|
||||
super(id);
|
||||
}
|
||||
constructor(public readonly executableId: ExecutableId) { }
|
||||
|
||||
public canRevert(): boolean {
|
||||
if (this.isReversible === undefined) {
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||
import type { RepositoryEntityId } from '@/application/Repository/RepositoryEntity';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
|
||||
export class SelectedScriptStub implements SelectedScript {
|
||||
public readonly script: Script;
|
||||
|
||||
public readonly id: string;
|
||||
public readonly id: RepositoryEntityId;
|
||||
|
||||
public revert: boolean;
|
||||
|
||||
constructor(
|
||||
script: Script,
|
||||
) {
|
||||
this.id = script.id;
|
||||
this.id = script.executableId;
|
||||
this.script = script;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { TreeInputNodeData } from '@/presentation/components/Scripts/View/Tree/TreeView/Bindings/TreeInputNodeData';
|
||||
import type { TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
|
||||
export class TreeInputNodeDataStub implements TreeInputNodeData {
|
||||
public id = 'stub-id';
|
||||
public id: TreeNodeId = 'stub-id';
|
||||
|
||||
public children?: readonly TreeInputNodeData[];
|
||||
|
||||
@@ -14,7 +15,7 @@ export class TreeInputNodeDataStub implements TreeInputNodeData {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
public withId(id: TreeNodeId): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { HierarchyAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/Hierarchy/HierarchyAccess';
|
||||
import type { TreeNodeStateAccess } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/State/StateAccess';
|
||||
import type { TreeNode } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import type { TreeNode, TreeNodeId } from '@/presentation/components/Scripts/View/Tree/TreeView/Node/TreeNode';
|
||||
import { NodeMetadataStub } from './NodeMetadataStub';
|
||||
import { HierarchyAccessStub } from './HierarchyAccessStub';
|
||||
import { TreeNodeStateAccessStub } from './TreeNodeStateAccessStub';
|
||||
@@ -39,7 +39,7 @@ export class TreeNodeStub implements TreeNode {
|
||||
return this;
|
||||
}
|
||||
|
||||
public withId(id: string): this {
|
||||
public withId(id: TreeNodeId): this {
|
||||
this.id = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user