Add type validation for parameters and fix types

This commit introduces type validation for parameter values within the
parser/compiler, aligning with the YAML schema. It aims to eliminate
dependencies on side effects in the collection files.

This update changes the treatment of data types in the Windows
collection, moving away from unintended type casting by the compiler.
Previously, numeric and boolean values were used even though only
string types were supported. This behavior was unstable and untested,
and has now been adjusted to use strings exclusively.

Changes ensure that parameter values are correctly validated
as strings, enhancing stability and maintainability.
This commit is contained in:
undergroundwires
2024-06-19 17:01:27 +02:00
parent 48761f62a2
commit fac26a6ca0
43 changed files with 873 additions and 466 deletions

View File

@@ -0,0 +1,51 @@
import { describe, it, expect } from 'vitest';
import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
describe('ParameterNameValidator', () => {
describe('accepts when valid', () => {
// arrange
const validValues: readonly string[] = [
'lowercase',
'onlyLetters',
'l3tt3rsW1thNumb3rs',
];
validValues.forEach((validValue) => {
it(validValue, () => {
// act
const act = () => validateParameterName(validValue);
// assert
expect(act).to.not.throw();
});
});
});
describe('throws if invalid', () => {
// arrange
const testScenarios: readonly {
readonly description: string;
readonly value: string;
}[] = [
{
description: 'empty name',
value: '',
},
{
description: 'has @',
value: 'b@d',
},
{
description: 'has {',
value: 'b{a}d',
},
];
testScenarios.forEach((
{ description, value },
) => {
it(description, () => {
// act
const act = () => validateParameterName(value);
// assert
expect(act).to.throw();
});
});
});
});

View File

@@ -1,6 +1,6 @@
import { describe, it, expect } from 'vitest';
import { createTypeValidator } from '@/application/Parser/Common/TypeValidator';
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { createTypeValidator, type NonEmptyStringAssertion, type RegexValidationRule } from '@/application/Parser/Common/TypeValidator';
import { itEachAbsentObjectValue, itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
describe('createTypeValidator', () => {
describe('assertObject', () => {
@@ -146,6 +146,108 @@ describe('createTypeValidator', () => {
});
});
});
describe('assertNonEmptyString', () => {
describe('with valid string', () => {
it('accepts non-empty string without regex rule', () => {
// arrange
const nonEmptyString = 'hello';
const { assertNonEmptyString } = createTypeValidator();
// act
const act = () => assertNonEmptyString({ value: nonEmptyString, valueName: 'unimportant name' });
// assert
expect(act).to.not.throw();
});
it('accepts if the string matches the regex', () => {
// arrange
const regex: RegExp = /goodbye/;
const stringMatchingRegex = 'Valid string containing "goodbye"';
const rule: RegexValidationRule = {
expectedMatch: regex,
errorMessage: 'String contain "goodbye"',
};
const { assertNonEmptyString } = createTypeValidator();
// act
const act = () => assertNonEmptyString({
value: stringMatchingRegex,
valueName: 'unimportant name',
rule,
});
// assert
expect(act).to.not.throw();
});
});
describe('with invalid string', () => {
describe('throws error for missing string', () => {
itEachAbsentStringValue((absentValue) => {
// arrange
const valueName = 'absent string value';
const expectedMessage = `'${valueName}' is missing.`;
const { assertNonEmptyString } = createTypeValidator();
// act
const act = () => assertNonEmptyString({ value: absentValue, valueName });
// assert
expect(act).to.throw(expectedMessage);
});
});
describe('throws error for non string values', () => {
// arrange
const testScenarios: readonly {
readonly description: string;
readonly invalidValue: unknown;
}[] = [
{
description: 'number',
invalidValue: 42,
},
{
description: 'boolean',
invalidValue: true,
},
{
description: 'object',
invalidValue: { property: 'value' },
},
{
description: 'array',
invalidValue: ['a', 'r', 'r', 'a', 'y'],
},
];
testScenarios.forEach(({
description, invalidValue,
}) => {
it(description, () => {
const valueName = 'invalidValue';
const expectedMessage = `'${valueName}' should be of type 'string', but is of type '${typeof invalidValue}'.`;
const { assertNonEmptyString } = createTypeValidator();
// act
const act = () => assertNonEmptyString({ value: invalidValue, valueName });
// assert
expect(act).to.throw(expectedMessage);
});
});
});
it('throws an error if the string does not match the regex', () => {
// arrange
const regex: RegExp = /goodbye/;
const stringNotMatchingRegex = 'Hello';
const expectedMessage = 'String should contain "goodbye"';
const rule: RegexValidationRule = {
expectedMatch: regex,
errorMessage: expectedMessage,
};
const assertion: NonEmptyStringAssertion = {
value: stringNotMatchingRegex,
valueName: 'non-important-value-name',
rule,
};
const { assertNonEmptyString } = createTypeValidator();
// act
const act = () => assertNonEmptyString(assertion);
// assert
expect(act).to.throw(expectedMessage);
});
});
});
});
function createObjectWithProperties(properties: readonly string[]): object {

View File

@@ -1,52 +1,110 @@
import { describe, expect } from 'vitest';
import { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { testParameterName } from '../../../ParameterNameTestRunner';
import { describe, expect, it } from 'vitest';
import { createFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { NonEmptyStringAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub';
import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
describe('FunctionCallArgument', () => {
describe('ctor', () => {
describe('createFunctionCallArgument', () => {
describe('parameter name', () => {
testParameterName(
(parameterName) => new FunctionCallArgumentBuilder()
.withParameterName(parameterName)
.build()
.parameterName,
);
});
describe('throws if argument value is absent', () => {
itEachAbsentStringValue((absentValue) => {
it('assigns correctly', () => {
// arrange
const parameterName = 'paramName';
const expectedError = `Missing argument value for the parameter "${parameterName}".`;
const argumentValue = absentValue;
const expectedName = 'expected parameter name';
const context = new TestContext()
.withParameterName(expectedName);
// act
const act = () => new FunctionCallArgumentBuilder()
.withParameterName(parameterName)
.withArgumentValue(argumentValue)
.build();
const actualArgument = context.create();
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
const actualName = actualArgument.parameterName;
expect(actualName).toEqual(expectedName);
});
it('validates parameter name', () => {
// arrange
const validator = createParameterNameValidatorStub();
const expectedParameterName = 'parameter name expected to be validated';
const context = new TestContext()
.withParameterName(expectedParameterName)
.withParameterNameValidator(validator.validator);
// act
context.create();
// assert
expect(validator.validatedNames).to.have.lengthOf(1);
expect(validator.validatedNames).to.include(expectedParameterName);
});
});
describe('argument value', () => {
it('assigns correctly', () => {
// arrange
const expectedValue = 'expected argument value';
const context = new TestContext()
.withArgumentValue(expectedValue);
// act
const actualArgument = context.create();
// assert
const actualValue = actualArgument.argumentValue;
expect(actualValue).toEqual(expectedValue);
});
it('validates argument value', () => {
// arrange
const parameterNameInError = 'expected parameter with argument error';
const expectedArgumentValue = 'argument value to be validated';
const expectedAssertion: NonEmptyStringAssertion = {
value: expectedArgumentValue,
valueName: `Missing argument value for the parameter "${parameterNameInError}".`,
};
const typeValidator = new TypeValidatorStub();
const context = new TestContext()
.withArgumentValue(expectedArgumentValue)
.withParameterName(parameterNameInError)
.withTypeValidator(typeValidator);
// act
context.create();
// assert
typeValidator.assertNonEmptyString(expectedAssertion);
});
});
});
});
class FunctionCallArgumentBuilder {
private parameterName = 'default-parameter-name';
class TestContext {
private parameterName = `[${TestContext.name}] default-parameter-name`;
private argumentValue = 'default-argument-value';
private argumentValue = `[${TestContext.name}] default-argument-value`;
public withParameterName(parameterName: string) {
private typeValidator: TypeValidator = new TypeValidatorStub();
private parameterNameValidator
: ParameterNameValidator = createParameterNameValidatorStub().validator;
public withParameterName(parameterName: string): this {
this.parameterName = parameterName;
return this;
}
public withArgumentValue(argumentValue: string) {
public withArgumentValue(argumentValue: string): this {
this.argumentValue = argumentValue;
return this;
}
public build() {
return new FunctionCallArgument(this.parameterName, this.argumentValue);
public withTypeValidator(typeValidator: TypeValidator): this {
this.typeValidator = typeValidator;
return this;
}
public withParameterNameValidator(parameterNameValidator: ParameterNameValidator): this {
this.parameterNameValidator = parameterNameValidator;
return this;
}
public create(): ReturnType<typeof createFunctionCallArgument> {
return createFunctionCallArgument(
this.parameterName,
this.argumentValue,
{
typeValidator: this.typeValidator,
validateParameterName: this.parameterNameValidator,
},
);
}
}

View File

@@ -2,7 +2,7 @@ import { describe, it, expect } from 'vitest';
import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgumentStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument';
import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
describe('FunctionCallArgumentCollection', () => {
describe('addArgument', () => {
@@ -25,7 +25,7 @@ describe('FunctionCallArgumentCollection', () => {
// arrange
const testCases: ReadonlyArray<{
readonly description: string;
readonly args: readonly IFunctionCallArgument[];
readonly args: readonly FunctionCallArgument[];
readonly expectedParameterNames: string[];
}> = [{
description: 'no args',

View File

@@ -14,6 +14,8 @@ import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFun
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentFactoryStub } from '../../../../../../../../../../../shared/Stubs/FunctionCallArgumentFactoryStub';
describe('NestedFunctionArgumentCompiler', () => {
describe('createCompiledNestedCall', () => {
@@ -266,6 +268,9 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
private wrapError: ErrorWithContextWrapper = errorWithContextWrapperStub;
private callArgumentFactory
: FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory;
public withExpressionsCompiler(expressionsCompiler: IExpressionsCompiler): this {
this.expressionsCompiler = expressionsCompiler;
return this;
@@ -292,10 +297,11 @@ class NestedFunctionArgumentCompilerBuilder implements ArgumentCompiler {
}
public createCompiledNestedCall(): FunctionCall {
const compiler = new NestedFunctionArgumentCompiler(
this.expressionsCompiler,
this.wrapError,
);
const compiler = new NestedFunctionArgumentCompiler({
expressionsCompiler: this.expressionsCompiler,
wrapError: this.wrapError,
createCallArgument: this.callArgumentFactory,
});
return compiler.createCompiledNestedCall(
this.nestedFunctionCall,
this.parentFunctionCall,

View File

@@ -3,8 +3,12 @@ import type { FunctionCallsData, FunctionCallData } from '@/application/collecti
import { parseFunctionCalls } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import type {
NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator,
} from '@/application/Parser/Common/TypeValidator';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub';
describe('FunctionCallsParser', () => {
describe('parseFunctionCalls', () => {
@@ -174,12 +178,15 @@ describe('FunctionCallsParser', () => {
});
class TestContext {
private validator: TypeValidator = new TypeValidatorStub();
private typeValidator: TypeValidator = new TypeValidatorStub();
private createCallArgument
: FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory;
private calls: FunctionCallsData = [new FunctionCallDataStub()];
public withTypeValidator(typeValidator: TypeValidator): this {
this.validator = typeValidator;
this.typeValidator = typeValidator;
return this;
}
@@ -191,7 +198,10 @@ class TestContext {
public parse(): ReturnType<typeof parseFunctionCalls> {
return parseFunctionCalls(
this.calls,
this.validator,
{
typeValidator: this.typeValidator,
createCallArgument: this.createCallArgument,
},
);
}
}

View File

@@ -1,50 +0,0 @@
import { describe, it, expect } from 'vitest';
import { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter';
import { testParameterName } from '../../ParameterNameTestRunner';
describe('FunctionParameter', () => {
describe('name', () => {
testParameterName(
(parameterName) => new FunctionParameterBuilder()
.withName(parameterName)
.build()
.name,
);
});
describe('isOptional', () => {
describe('sets as expected', () => {
// arrange
const expectedValues = [true, false];
for (const expected of expectedValues) {
it(expected.toString(), () => {
// act
const sut = new FunctionParameterBuilder()
.withIsOptional(expected)
.build();
// expect
expect(sut.isOptional).to.equal(expected);
});
}
});
});
});
class FunctionParameterBuilder {
private name = 'parameterFromParameterBuilder';
private isOptional = false;
public withName(name: string) {
this.name = name;
return this;
}
public withIsOptional(isOptional: boolean) {
this.isOptional = isOptional;
return this;
}
public build() {
return new FunctionParameter(this.name, this.isOptional);
}
}

View File

@@ -0,0 +1,83 @@
import { describe, it, expect } from 'vitest';
import type { ParameterDefinitionData } from '@/application/collections/';
import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
import { createParameterNameValidatorStub } from '@tests/unit/shared/Stubs/ParameterNameValidatorStub';
import { parseFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser';
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
describe('FunctionParameterParser', () => {
describe('parseFunctionParameter', () => {
describe('name', () => {
it('assigns correctly', () => {
// arrange
const expectedName = 'expected-function-name';
const data = new ParameterDefinitionDataStub()
.withName(expectedName);
// act
const actual = new TestContext()
.withData(data)
.parse();
// expect
const actualName = actual.name;
expect(actualName).to.equal(expectedName);
});
it('validates correctly', () => {
// arrange
const expectedName = 'expected-function-name';
const { validator, validatedNames } = createParameterNameValidatorStub();
const data = new ParameterDefinitionDataStub()
.withName(expectedName);
// act
new TestContext()
.withData(data)
.withValidator(validator)
.parse();
// expect
expect(validatedNames).to.have.lengthOf(1);
expect(validatedNames).to.contain(expectedName);
});
});
describe('isOptional', () => {
describe('assigns correctly', () => {
// arrange
const expectedValues = [true, false];
for (const expected of expectedValues) {
it(expected.toString(), () => {
const data = new ParameterDefinitionDataStub()
.withOptionality(expected);
// act
const actual = new TestContext()
.withData(data)
.parse();
// expect
expect(actual.isOptional).to.equal(expected);
});
}
});
});
});
});
class TestContext {
private data: ParameterDefinitionData = new ParameterDefinitionDataStub()
.withName(`[${TestContext.name}]function-name`);
private validator: ParameterNameValidator = createParameterNameValidatorStub().validator;
public withData(data: ParameterDefinitionData) {
this.data = data;
return this;
}
public withValidator(parameterNameValidator: ParameterNameValidator): this {
this.validator = parameterNameValidator;
return this;
}
public parse() {
return parseFunctionParameter(
this.data,
this.validator,
);
}
}

View File

@@ -4,7 +4,7 @@ import type {
ParameterDefinitionData, FunctionCallsData,
} from '@/application/collections/';
import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import { parseSharedFunctions, type FunctionParameterFactory } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
import { parseSharedFunctions } from '@/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser';
import { createFunctionDataWithCall, createFunctionDataWithCode, createFunctionDataWithoutCallOrCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
import { ParameterDefinitionDataStub } from '@tests/unit/shared/Stubs/ParameterDefinitionDataStub';
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
@@ -16,7 +16,6 @@ import type { ICodeValidator } from '@/application/Parser/Executable/Script/Vali
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
@@ -26,6 +25,8 @@ import { createFunctionCallsParserStub } from '@tests/unit/shared/Stubs/Function
import { FunctionCallStub } from '@tests/unit/shared/Stubs/FunctionCallStub';
import type { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser';
import { createFunctionParameterParserStub } from '@tests/unit/shared/Stubs/FunctionParameterParserStub';
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
describe('SharedFunctionsParser', () => {
@@ -185,7 +186,7 @@ describe('SharedFunctionsParser', () => {
const functionName = 'functionName';
const expectedErrorMessage = `Failed to create parameter: ${invalidParameterName} for function "${functionName}"`;
const expectedInnerError = new Error('injected error');
const parameterFactory: FunctionParameterFactory = () => {
const parser: FunctionParameterParser = () => {
throw expectedInnerError;
};
const functionData = createFunctionDataWithCode()
@@ -196,7 +197,7 @@ describe('SharedFunctionsParser', () => {
throwingAction: (wrapError) => {
new TestContext()
.withFunctions([functionData])
.withFunctionParameterFactory(parameterFactory)
.withFunctionParameterParser(parser)
.withErrorWrapper(wrapError)
.parseFunctions();
},
@@ -415,12 +416,7 @@ class TestContext {
private functionCallsParser: FunctionCallsParser = createFunctionCallsParserStub().parser;
private parameterFactory: FunctionParameterFactory = (
name: string,
isOptional: boolean,
) => new FunctionParameterStub()
.withName(name)
.withOptional(isOptional);
private functionParameterParser: FunctionParameterParser = createFunctionParameterParserStub;
private parameterCollectionFactory
: FunctionParameterCollectionFactory = () => new FunctionParameterCollectionStub();
@@ -450,8 +446,8 @@ class TestContext {
return this;
}
public withFunctionParameterFactory(parameterFactory: FunctionParameterFactory): this {
this.parameterFactory = parameterFactory;
public withFunctionParameterParser(functionParameterParser: FunctionParameterParser): this {
this.functionParameterParser = functionParameterParser;
return this;
}
@@ -469,7 +465,7 @@ class TestContext {
{
codeValidator: this.codeValidator,
wrapError: this.wrapError,
createParameter: this.parameterFactory,
parseParameter: this.functionParameterParser,
createParameterCollection: this.parameterCollectionFactory,
parseFunctionCalls: this.functionCallsParser,
},

View File

@@ -1,54 +0,0 @@
import { describe, it, expect } from 'vitest';
export function testParameterName(action: (parameterName: string) => string) {
describe('name', () => {
describe('sets as expected', () => {
// arrange
const expectedValues: readonly string[] = [
'lowercase',
'onlyLetters',
'l3tt3rsW1thNumb3rs',
];
for (const expected of expectedValues) {
it(expected, () => {
// act
const value = action(expected);
// assert
expect(value).to.equal(expected);
});
}
});
describe('throws if invalid', () => {
// arrange
const testScenarios: readonly {
readonly description: string;
readonly value: string;
readonly expectedError: string;
}[] = [
{
description: 'empty Name',
value: '',
expectedError: 'missing parameter name',
},
{
description: 'has @',
value: 'b@d',
expectedError: 'parameter name must be alphanumeric but it was "b@d"',
},
{
description: 'has {',
value: 'b{a}d',
expectedError: 'parameter name must be alphanumeric but it was "b{a}d"',
},
];
for (const { description, value, expectedError } of testScenarios) {
it(description, () => {
// act
const act = () => action(value);
// assert
expect(act).to.throw(expectedError);
});
}
});
});
}

View File

@@ -0,0 +1,24 @@
import { describe, it } from 'vitest';
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
import { validateParameterName } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
import type { NonEmptyStringAssertion } from '@/application/Parser/Common/TypeValidator';
describe('ParameterNameValidator', () => {
it('asserts correctly', () => {
// arrange
const parameterName = 'expected-parameter-name';
const validator = new TypeValidatorStub();
const expectedAssertion: NonEmptyStringAssertion = {
value: parameterName,
valueName: 'parameter name',
rule: {
expectedMatch: /^[0-9a-zA-Z]+$/,
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
},
};
// act
validateParameterName(parameterName, validator);
// assert
validator.assertNonEmptyString(expectedAssertion);
});
});

View File

@@ -1,91 +1,120 @@
import { describe, it, expect } from 'vitest';
import { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { ExpressionsCompilerStub } from '@tests/unit/shared/Stubs/ExpressionsCompilerStub';
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
import { substituteCode } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentFactoryStub } from '@tests/unit/shared/Stubs/FunctionCallArgumentFactoryStub';
describe('CodeSubstituter', () => {
describe('throws if code is empty', () => {
itEachAbsentStringValue((emptyCode) => {
// arrange
const expectedError = 'missing code';
const code = emptyCode;
const projectDetails = new ProjectDetailsStub();
const sut = new CodeSubstituterBuilder().build();
// act
const act = () => sut.substitute(code, projectDetails);
// assert
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('substitutes parameters as expected values', () => {
// arrange
const projectDetails = new ProjectDetailsStub();
const date = new Date();
const testCases: Array<{ parameter: string, argument: string }> = [
{
parameter: 'homepage',
argument: projectDetails.homepage,
},
{
parameter: 'version',
argument: projectDetails.version.toString(),
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.withDate(date)
.build();
describe('substituteCode', () => {
describe('throws if code is empty', () => {
itEachAbsentStringValue((emptyCode) => {
// arrange
const expectedError = 'missing code';
const context = new TestContext()
.withCode(emptyCode);
// act
sut.substitute('non empty code', projectDetails);
const act = () => context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const sut = new CodeSubstituterBuilder()
.withCompiler(compilerStub)
.build();
// act
sut.substitute(expected, new ProjectDetailsStub());
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
expect(act).to.throw(expectedError);
}, { excludeNull: true, excludeUndefined: true });
});
describe('substitutes parameters as expected values', () => {
// arrange
const projectDetails = new ProjectDetailsStub();
const date = new Date();
const testCases: Array<{ parameter: string, argument: string }> = [
{
parameter: 'homepage',
argument: projectDetails.homepage,
},
{
parameter: 'version',
argument: projectDetails.version.toString(),
},
{
parameter: 'date',
argument: date.toUTCString(),
},
];
for (const testCase of testCases) {
it(`substitutes ${testCase.parameter} as expected`, () => {
const compilerStub = new ExpressionsCompilerStub();
const context = new TestContext()
.withCompiler(compilerStub)
.withDate(date)
.withProjectDetails(projectDetails);
// act
context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
const parameters = compilerStub.callHistory[0].args[1];
expect(parameters.hasArgument(testCase.parameter));
const { argumentValue } = parameters.getArgument(testCase.parameter);
expect(argumentValue).to.equal(testCase.argument);
});
}
});
it('returns code as it is', () => {
// arrange
const expected = 'expected-code';
const compilerStub = new ExpressionsCompilerStub();
const context = new TestContext()
.withCompiler(compilerStub)
.withCode(expected);
// act
context.substitute();
// assert
expect(compilerStub.callHistory).to.have.lengthOf(1);
expect(compilerStub.callHistory[0].args[0]).to.equal(expected);
});
});
});
class CodeSubstituterBuilder {
class TestContext {
private compiler: IExpressionsCompiler = new ExpressionsCompilerStub();
private date = new Date();
public withCompiler(compiler: IExpressionsCompiler) {
private code = `[${TestContext.name}] default code for testing`;
private projectDetails: ProjectDetails = new ProjectDetailsStub();
private callArgumentFactory
: FunctionCallArgumentFactory = new FunctionCallArgumentFactoryStub().factory;
public withCompiler(compiler: IExpressionsCompiler): this {
this.compiler = compiler;
return this;
}
public withDate(date: Date) {
public withDate(date: Date): this {
this.date = date;
return this;
}
public build() {
return new CodeSubstituter(this.compiler, this.date);
public withCode(code: string): this {
this.code = code;
return this;
}
public withProjectDetails(projectDetails: ProjectDetails): this {
this.projectDetails = projectDetails;
return this;
}
public substitute(): ReturnType<typeof substituteCode> {
return substituteCode(
this.code,
this.projectDetails,
{
compiler: this.compiler,
provideDate: () => this.date,
createCallArgument: this.callArgumentFactory,
},
);
}
}

View File

@@ -1,7 +1,7 @@
import { describe, it, expect } from 'vitest';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import type { EnumParser } from '@/application/Common/Enum';
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
@@ -83,7 +83,7 @@ describe('ScriptingDefinitionParser', () => {
const context = new TestContext()
.withData(data)
.withProjectDetails(projectDetails)
.withSubstituter(substituterMock);
.withSubstituter(substituterMock.substitute);
// act
const definition = context.parseScriptingDefinition();
// assert
@@ -99,7 +99,7 @@ class TestContext {
private languageParser: EnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
.setupDefaultValue(ScriptingLanguage.shellscript);
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
private codeSubstituter: CodeSubstituter = new CodeSubstituterStub().substitute;
private validator: TypeValidator = new TypeValidatorStub();
@@ -122,7 +122,7 @@ class TestContext {
return this;
}
public withSubstituter(substituter: ICodeSubstituter): this {
public withSubstituter(substituter: CodeSubstituter): this {
this.codeSubstituter = substituter;
return this;
}

View File

@@ -1,22 +1,22 @@
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
import type { CodeSubstituter } from '@/application/Parser/ScriptingDefinition/CodeSubstituter';
export class CodeSubstituterStub implements ICodeSubstituter {
export class CodeSubstituterStub {
private readonly scenarios = new Array<{
code: string, projectDetails: ProjectDetails, result: string }>();
public substitute(code: string, projectDetails: ProjectDetails): string {
public setup(code: string, projectDetails: ProjectDetails, result: string) {
this.scenarios.push({ code, projectDetails, result });
return this;
}
public substitute: CodeSubstituter = (code: string, projectDetails: ProjectDetails) => {
const scenario = this.scenarios.find(
(s) => s.code === code && s.projectDetails === projectDetails,
);
if (scenario) {
return scenario.result;
}
return `[CodeSubstituterStub] - code: ${code}`;
}
public setup(code: string, projectDetails: ProjectDetails, result: string) {
this.scenarios.push({ code, projectDetails, result });
return this;
}
return `[${CodeSubstituterStub.name}] - code: ${code}`;
};
}

View File

@@ -1,9 +1,9 @@
import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument';
import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import type { IFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
private args = new Array<FunctionCallArgument>();
public withEmptyArguments(): this {
this.args.length = 0;
@@ -36,7 +36,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
return this.args.some((a) => a.parameterName === parameterName);
}
public addArgument(argument: IFunctionCallArgument): void {
public addArgument(argument: FunctionCallArgument): void {
this.args.push(argument);
}
@@ -44,7 +44,7 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
return this.args.map((a) => a.parameterName);
}
public getArgument(parameterName: string): IFunctionCallArgument {
public getArgument(parameterName: string): FunctionCallArgument {
const arg = this.args.find((a) => a.parameterName === parameterName);
if (!arg) {
throw new Error(`no argument exists for parameter "${parameterName}"`);

View File

@@ -0,0 +1,10 @@
import type { FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentFactoryStub {
public factory: FunctionCallArgumentFactory = (parameterName, argumentValue) => {
return new FunctionCallArgumentStub()
.withParameterName(parameterName)
.withArgumentValue(argumentValue);
};
}

View File

@@ -1,6 +1,6 @@
import type { IFunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgument';
import type { FunctionCallArgument } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
export class FunctionCallArgumentStub implements IFunctionCallArgument {
export class FunctionCallArgumentStub implements FunctionCallArgument {
public parameterName = 'stub-parameter-name';
public argumentValue = 'stub-arg-name';

View File

@@ -1,15 +1,15 @@
import type { IFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter';
import type { IFunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import type { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter';
import { FunctionParameterStub } from './FunctionParameterStub';
export class FunctionParameterCollectionStub implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>();
private parameters = new Array<FunctionParameter>();
public addParameter(parameter: IFunctionParameter): void {
public addParameter(parameter: FunctionParameter): void {
this.parameters.push(parameter);
}
public get all(): readonly IFunctionParameter[] {
public get all(): readonly FunctionParameter[] {
return this.parameters;
}

View File

@@ -0,0 +1,8 @@
import type { FunctionParameterParser } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterParser';
import { FunctionParameterStub } from './FunctionParameterStub';
export const createFunctionParameterParserStub: FunctionParameterParser = (parameters) => {
return new FunctionParameterStub()
.withName(parameters.name)
.withOptional(parameters.optional || false);
};

View File

@@ -1,6 +1,6 @@
import type { IFunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/IFunctionParameter';
import type { FunctionParameter } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameter';
export class FunctionParameterStub implements IFunctionParameter {
export class FunctionParameterStub implements FunctionParameter {
public name = 'function-parameter-stub';
public isOptional = true;

View File

@@ -0,0 +1,12 @@
import type { ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
export const createParameterNameValidatorStub = () => {
const validatedNames = new Array<string>();
const validator: ParameterNameValidator = (name) => {
validatedNames.push(name);
};
return {
validator,
validatedNames,
};
};

View File

@@ -1,4 +1,7 @@
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
import type {
NonEmptyCollectionAssertion, NonEmptyStringAssertion,
ObjectAssertion, TypeValidator,
} from '@/application/Parser/Common/TypeValidator';
import type { FunctionKeys } from '@/TypeHelpers';
import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes';
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
@@ -22,6 +25,13 @@ export class TypeValidatorStub
});
}
public assertNonEmptyString(assertion: NonEmptyStringAssertion): void {
this.registerMethodCall({
methodName: 'assertNonEmptyString',
args: [assertion],
});
}
public expectObjectAssertion<T>(
expectedAssertion: ObjectAssertion<T>,
): void {
@@ -34,6 +44,12 @@ export class TypeValidatorStub
this.expectAssertion('assertNonEmptyCollection', expectedAssertion);
}
public expectNonEmptyStringAssertion(
expectedAssertion: NonEmptyStringAssertion,
): void {
this.expectAssertion('assertNonEmptyString', expectedAssertion);
}
private expectAssertion<T extends FunctionKeys<TypeValidator>>(
methodName: T,
expectedAssertion: Parameters<TypeValidator[T]>[0],