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,6 +1,6 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type { CategoryData, ExecutableData } from '@/application/collections/';
|
||||
import { type CategoryFactory, parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
||||
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
||||
import { type ScriptParser } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||
@@ -20,14 +20,48 @@ import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||
import { indentText } from '@/application/Common/Text/IndentText';
|
||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
||||
import type { CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
||||
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
||||
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
||||
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
||||
|
||||
describe('CategoryParser', () => {
|
||||
describe('parseCategory', () => {
|
||||
describe('validation', () => {
|
||||
describe('validates for name', () => {
|
||||
describe('id', () => {
|
||||
it('creates ID correctly', () => {
|
||||
// arrange
|
||||
const expectedId: ExecutableId = 'expected-id';
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withName(expectedId);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualScript = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualId = getInitParameters(actualScript)?.executableId;
|
||||
expect(actualId).to.equal(expectedId);
|
||||
});
|
||||
});
|
||||
describe('name', () => {
|
||||
it('parses name correctly', () => {
|
||||
// arrange
|
||||
const expectedName = 'test-expected-name';
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withName(expectedName);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualName = getInitParameters(actualCategory)?.name;
|
||||
expect(actualName).to.equal(expectedName);
|
||||
});
|
||||
describe('validates name', () => {
|
||||
// arrange
|
||||
const expectedName = 'expected category name to be validated';
|
||||
const category = new CategoryDataStub()
|
||||
@@ -38,7 +72,7 @@ describe('CategoryParser', () => {
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -49,7 +83,33 @@ describe('CategoryParser', () => {
|
||||
};
|
||||
});
|
||||
});
|
||||
describe('validates for unknown object', () => {
|
||||
});
|
||||
describe('docs', () => {
|
||||
it('parses docs correctly', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withDocs(url);
|
||||
const parseDocs: DocsParser = (data) => {
|
||||
return [
|
||||
`parsed docs: ${JSON.stringify(data)}`,
|
||||
];
|
||||
};
|
||||
const expectedDocs = parseDocs(categoryData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withDocsParser(parseDocs)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||
});
|
||||
});
|
||||
describe('property validation', () => {
|
||||
describe('validates for unknown executable', () => {
|
||||
// arrange
|
||||
const category = new CategoryDataStub();
|
||||
const expectedContext: CategoryErrorContext = {
|
||||
@@ -63,7 +123,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -90,7 +150,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -102,6 +162,8 @@ describe('CategoryParser', () => {
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('children', () => {
|
||||
describe('validates children for non-empty collection', () => {
|
||||
// arrange
|
||||
const category = new CategoryDataStub()
|
||||
@@ -117,7 +179,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(category)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -167,7 +229,7 @@ describe('CategoryParser', () => {
|
||||
parentCategory: parent,
|
||||
};
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -201,7 +263,7 @@ describe('CategoryParser', () => {
|
||||
itValidatesType(
|
||||
(validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -231,7 +293,7 @@ describe('CategoryParser', () => {
|
||||
};
|
||||
itValidatesName((validatorFactory) => {
|
||||
// act
|
||||
new TestBuilder()
|
||||
new TestContext()
|
||||
.withData(parent)
|
||||
.withValidatorFactory(validatorFactory)
|
||||
.parseCategory();
|
||||
@@ -243,178 +305,169 @@ describe('CategoryParser', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('rethrows exception if category factory fails', () => {
|
||||
// arrange
|
||||
const givenData = new CategoryDataStub();
|
||||
const expectedContextMessage = 'Failed to parse category.';
|
||||
const expectedError = new Error();
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const validatorStub = new ExecutableValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
const factoryMock: CategoryFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestBuilder()
|
||||
.withCategoryFactory(factoryMock)
|
||||
.withValidatorFactory(() => validatorStub)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withData(givenData)
|
||||
describe('parses correct subscript', () => {
|
||||
it('parses single script correctly', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected script');
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childScriptData = createScriptDataWithCode();
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren([childScriptData]);
|
||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
it('parses docs correctly', () => {
|
||||
// arrange
|
||||
const url = 'https://privacy.sexy';
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withDocs(url);
|
||||
const parseDocs: DocsParser = (data) => {
|
||||
return [
|
||||
`parsed docs: ${JSON.stringify(data)}`,
|
||||
];
|
||||
};
|
||||
const expectedDocs = parseDocs(categoryData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withDocsParser(parseDocs)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualDocs = getInitParameters(actualCategory)?.docs;
|
||||
expect(actualDocs).to.deep.equal(expectedDocs);
|
||||
});
|
||||
describe('parses expected subscript', () => {
|
||||
it('parses single script correctly', () => {
|
||||
// arrange
|
||||
const expectedScript = new ScriptStub('expected script');
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childScriptData = createScriptDataWithCode();
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren([childScriptData]);
|
||||
scriptParser.setupParsedResultForData(childScriptData, expectedScript);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.have.lengthOf(1);
|
||||
const actualScript = actualScripts[0];
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
it('parses multiple scripts correctly', () => {
|
||||
// arrange
|
||||
const expectedScripts = [
|
||||
new ScriptStub('expected-first-script'),
|
||||
new ScriptStub('expected-second-script'),
|
||||
];
|
||||
const childrenData = [
|
||||
createScriptDataWithCall(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const scriptParser = new ScriptParserStub();
|
||||
childrenData.forEach((_, index) => {
|
||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||
// assert
|
||||
const actualScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualScripts);
|
||||
expect(actualScripts).to.have.lengthOf(1);
|
||||
const actualScript = actualScripts[0];
|
||||
expect(actualScript).to.equal(expectedScript);
|
||||
});
|
||||
it('parses multiple scripts correctly', () => {
|
||||
// arrange
|
||||
const expectedScripts = [
|
||||
new ScriptStub('expected-first-script'),
|
||||
new ScriptStub('expected-second-script'),
|
||||
];
|
||||
const childrenData = [
|
||||
createScriptDataWithCall(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const scriptParser = new ScriptParserStub();
|
||||
childrenData.forEach((_, index) => {
|
||||
scriptParser.setupParsedResultForData(childrenData[index], expectedScripts[index]);
|
||||
});
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||
});
|
||||
it('parses all scripts with correct utilities', () => {
|
||||
// arrange
|
||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCollectionUtilities(expected)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualUtilities = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
actualUtilities.every(
|
||||
(actual) => actual === expected,
|
||||
),
|
||||
formatAssertionMessage([
|
||||
`Expected all elements to be ${JSON.stringify(expected)}`,
|
||||
'All elements:',
|
||||
indentText(JSON.stringify(actualUtilities)),
|
||||
]),
|
||||
).to.equal(true);
|
||||
});
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
expect(actualParsedScripts.length).to.equal(expectedScripts.length);
|
||||
expect(actualParsedScripts).to.have.members(expectedScripts);
|
||||
});
|
||||
it('parses all scripts with correct utilities', () => {
|
||||
it('parses correct subcategories', () => {
|
||||
// arrange
|
||||
const expected = new CategoryCollectionSpecificUtilitiesStub();
|
||||
const scriptParser = new ScriptParserStub();
|
||||
const childrenData = [
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
createScriptDataWithCode(),
|
||||
];
|
||||
const expectedChildCategory = new CategoryStub('expected-child-category');
|
||||
const childCategoryData = new CategoryDataStub()
|
||||
.withName('expected child category')
|
||||
.withChildren([createScriptDataWithCode()]);
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withChildren(childrenData);
|
||||
.withName('category name')
|
||||
.withChildren([childCategoryData]);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
const actualCategory = new TestContext()
|
||||
.withData(categoryData)
|
||||
.withCollectionUtilities(expected)
|
||||
.withScriptParser(scriptParser.get())
|
||||
.withCategoryFactory(categoryFactorySpy)
|
||||
.withCategoryFactory((parameters) => {
|
||||
if (parameters.name === childCategoryData.category) {
|
||||
return expectedChildCategory;
|
||||
}
|
||||
return categoryFactorySpy(parameters);
|
||||
})
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualParsedScripts = getInitParameters(actualCategory)?.scripts;
|
||||
expectExists(actualParsedScripts);
|
||||
const actualUtilities = actualParsedScripts.map(
|
||||
(s) => scriptParser.getParseParameters(s)[1],
|
||||
);
|
||||
expect(
|
||||
actualUtilities.every(
|
||||
(actual) => actual === expected,
|
||||
),
|
||||
formatAssertionMessage([
|
||||
`Expected all elements to be ${JSON.stringify(expected)}`,
|
||||
'All elements:',
|
||||
indentText(JSON.stringify(actualUtilities)),
|
||||
]),
|
||||
).to.equal(true);
|
||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||
expectExists(actualSubcategories);
|
||||
expect(actualSubcategories).to.have.lengthOf(1);
|
||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||
});
|
||||
});
|
||||
it('returns expected subcategories', () => {
|
||||
// arrange
|
||||
const expectedChildCategory = new CategoryStub(33);
|
||||
const childCategoryData = new CategoryDataStub()
|
||||
.withName('expected child category')
|
||||
.withChildren([createScriptDataWithCode()]);
|
||||
const categoryData = new CategoryDataStub()
|
||||
.withName('category name')
|
||||
.withChildren([childCategoryData]);
|
||||
const { categoryFactorySpy, getInitParameters } = createCategoryFactorySpy();
|
||||
// act
|
||||
const actualCategory = new TestBuilder()
|
||||
.withData(categoryData)
|
||||
.withCategoryFactory((parameters) => {
|
||||
if (parameters.name === childCategoryData.category) {
|
||||
return expectedChildCategory;
|
||||
}
|
||||
return categoryFactorySpy(parameters);
|
||||
})
|
||||
.parseCategory();
|
||||
// assert
|
||||
const actualSubcategories = getInitParameters(actualCategory)?.subcategories;
|
||||
expectExists(actualSubcategories);
|
||||
expect(actualSubcategories).to.have.lengthOf(1);
|
||||
expect(actualSubcategories[0]).to.equal(expectedChildCategory);
|
||||
describe('category creation', () => {
|
||||
it('creates category from the factory', () => {
|
||||
// arrange
|
||||
const expectedCategory = new CategoryStub('expected-category');
|
||||
const categoryFactory: CategoryFactory = () => expectedCategory;
|
||||
// act
|
||||
const actualCategory = new TestContext()
|
||||
.withCategoryFactory(categoryFactory)
|
||||
.parseCategory();
|
||||
// assert
|
||||
expect(actualCategory).to.equal(expectedCategory);
|
||||
});
|
||||
describe('rethrows exception if category factory fails', () => {
|
||||
// arrange
|
||||
const givenData = new CategoryDataStub();
|
||||
const expectedContextMessage = 'Failed to parse category.';
|
||||
const expectedError = new Error();
|
||||
// act & assert
|
||||
itThrowsContextualError({
|
||||
throwingAction: (wrapError) => {
|
||||
const validatorStub = new ExecutableValidatorStub();
|
||||
validatorStub.createContextualErrorMessage = (message) => message;
|
||||
const factoryMock: CategoryFactory = () => {
|
||||
throw expectedError;
|
||||
};
|
||||
new TestContext()
|
||||
.withCategoryFactory(factoryMock)
|
||||
.withValidatorFactory(() => validatorStub)
|
||||
.withErrorWrapper(wrapError)
|
||||
.withData(givenData)
|
||||
.parseCategory();
|
||||
},
|
||||
expectedWrappedError: expectedError,
|
||||
expectedContextMessage,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class TestBuilder {
|
||||
class TestContext {
|
||||
private data: CategoryData = new CategoryDataStub();
|
||||
|
||||
private collectionUtilities:
|
||||
CategoryCollectionSpecificUtilitiesStub = new CategoryCollectionSpecificUtilitiesStub();
|
||||
|
||||
private categoryFactory: CategoryFactory = () => new CategoryStub(33);
|
||||
private categoryFactory: CategoryFactory = createCategoryFactorySpy().categoryFactorySpy;
|
||||
|
||||
private errorWrapper: ErrorWithContextWrapper = new ErrorWrapperStub().get();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user