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

@@ -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,
},
);
}
}