Add object property validation in parser #369

This commit introduces stricter type validation across the application
to reject objects with unexpected properties, enhancing the robustness
and predictability of data handling.

Changes include:

- Implement a common utility to validate object types.
- Refactor across various parsers and data handlers to utilize the new
  validations.
- Update error messages for better clarity and troubleshooting.
This commit is contained in:
undergroundwires
2024-06-13 22:26:57 +02:00
parent c138f74460
commit 6ecfa9b954
43 changed files with 1215 additions and 466 deletions

View File

@@ -1,32 +1,52 @@
import { describe, it, expect } from 'vitest';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import type { IEnumParser } from '@/application/Common/Enum';
import type { EnumParser } from '@/application/Common/Enum';
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub';
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
import type { ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { ScriptingDefinitionData } from '@/application/collections/';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
describe('ScriptingDefinitionParser', () => {
describe('parseScriptingDefinition', () => {
it('validates data', () => {
// arrange
const data = new ScriptingDefinitionDataStub();
const expectedAssertion: ObjectAssertion<ScriptingDefinitionData> = {
value: data,
valueName: 'scripting definition',
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
};
const validatorStub = new TypeValidatorStub();
const context = new TestContext()
.withTypeValidator(validatorStub)
.withData(data);
// act
context.parseScriptingDefinition();
// assert
validatorStub.assertObject(expectedAssertion);
});
describe('language', () => {
it('parses as expected', () => {
// arrange
const expectedLanguage = ScriptingLanguage.batchfile;
const languageText = 'batchfile';
const expectedName = 'language';
const projectDetails = new ProjectDetailsStub();
const definition = new ScriptingDefinitionDataStub()
.withLanguage(languageText);
const parserMock = new EnumParserStub<ScriptingLanguage>()
.setup(expectedName, languageText, expectedLanguage);
const sut = new ScriptingDefinitionParserBuilder()
const context = new TestContext()
.withParser(parserMock)
.build();
.withData(definition);
// act
const actual = sut.parse(definition, projectDetails);
const actual = context.parseScriptingDefinition();
// assert
expect(actual.language).to.equal(expectedLanguage);
});
@@ -35,56 +55,92 @@ describe('ScriptingDefinitionParser', () => {
// arrange
const code = 'hello';
const expected = 'substituted';
const testCases = [
const testScenarios: readonly {
readonly description: string;
getActualValue(result: IScriptingDefinition): string;
readonly data: ScriptingDefinitionData;
}[] = [
{
name: 'startCode',
description: 'startCode',
getActualValue: (result: IScriptingDefinition) => result.startCode,
data: new ScriptingDefinitionDataStub()
.withStartCode(code),
},
{
name: 'endCode',
description: 'endCode',
getActualValue: (result: IScriptingDefinition) => result.endCode,
data: new ScriptingDefinitionDataStub()
.withEndCode(code),
},
];
for (const testCase of testCases) {
it(testCase.name, () => {
testScenarios.forEach(({
description, data, getActualValue,
}) => {
it(description, () => {
const projectDetails = new ProjectDetailsStub();
const substituterMock = new CodeSubstituterStub()
.setup(code, projectDetails, expected);
const sut = new ScriptingDefinitionParserBuilder()
.withSubstituter(substituterMock)
.build();
const context = new TestContext()
.withData(data)
.withProjectDetails(projectDetails)
.withSubstituter(substituterMock);
// act
const definition = sut.parse(testCase.data, projectDetails);
const definition = context.parseScriptingDefinition();
// assert
const actual = testCase.getActualValue(definition);
const actual = getActualValue(definition);
expect(actual).to.equal(expected);
});
}
});
});
});
});
class ScriptingDefinitionParserBuilder {
private languageParser: IEnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
class TestContext {
private languageParser: EnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
.setupDefaultValue(ScriptingLanguage.shellscript);
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
public withParser(parser: IEnumParser<ScriptingLanguage>) {
private validator: TypeValidator = new TypeValidatorStub();
private data: ScriptingDefinitionData = new ScriptingDefinitionDataStub();
private projectDetails: ProjectDetails = new ProjectDetailsStub();
public withData(data: ScriptingDefinitionData): this {
this.data = data;
return this;
}
public withProjectDetails(projectDetails: ProjectDetails): this {
this.projectDetails = projectDetails;
return this;
}
public withParser(parser: EnumParser<ScriptingLanguage>): this {
this.languageParser = parser;
return this;
}
public withSubstituter(substituter: ICodeSubstituter) {
public withSubstituter(substituter: ICodeSubstituter): this {
this.codeSubstituter = substituter;
return this;
}
public build() {
return new ScriptingDefinitionParser(this.languageParser, this.codeSubstituter);
public withTypeValidator(validator: TypeValidator): this {
this.validator = validator;
return this;
}
public parseScriptingDefinition() {
return parseScriptingDefinition(
this.data,
this.projectDetails,
{
languageParser: this.languageParser,
codeSubstituter: this.codeSubstituter,
validator: this.validator,
},
);
}
}