Add optionality for parameters

This commit allows for parameters that does not require any arguments to
be provided in function calls. It changes collection syntax where
parameters are list of objects instead of primitive strings. A
parameter has now 'name' and 'optional' properties. 'name' is required
and used in same way as older strings as parameter definitions.
'Optional' property is optional, 'false' is the default behavior if
undefined. It also adds additional validation to restrict parameter
names to alphanumeric strings to have a clear syntax in expressions.
This commit is contained in:
undergroundwires
2021-09-02 18:59:25 +01:00
parent dcccb61781
commit 6a89c6224b
51 changed files with 1311 additions and 354 deletions

View File

@@ -1,11 +1,14 @@
import 'mocha';
import { expect } from 'chai';
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { FunctionData } from 'js-yaml-loader!@/*';
import { FunctionData, ParameterDefinitionData } from 'js-yaml-loader!@/*';
import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler';
import { FunctionCompiler } from '@/application/Parser/Script/Compiler/Function/FunctionCompiler';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionCallCompilerStub } from '@tests/unit/stubs/FunctionCallCompilerStub';
import { FunctionDataStub } from '@tests/unit/stubs/FunctionDataStub';
import { ParameterDefinitionDataStub } from '@tests/unit/stubs/ParameterDefinitionDataStub';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
describe('FunctionsCompiler', () => {
describe('compileFunctions', () => {
@@ -34,29 +37,31 @@ describe('FunctionsCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('throws when function parameters have same names', () => {
// arrange
const parameterName = 'duplicate-parameter';
const func = FunctionDataStub.createWithCall()
.withParameters(parameterName, parameterName);
const expectedError = `"${func.name}": duplicate parameter name: "${parameterName}"`;
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ func ]);
// assert
expect(act).to.throw(expectedError);
});
it('throws when parameters is not an array of strings', () => {
// arrange
const parameterNameWithUnexpectedType = 5;
const func = FunctionDataStub.createWithCall()
.withParameters(parameterNameWithUnexpectedType as any);
const expectedError = `unexpected parameter name type in "${func.name}"`;
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ func ]);
// assert
expect(act).to.throw(expectedError);
describe('throws when parameters type is not as expected', () => {
const testCases = [
{
state: 'when not an array',
invalidType: 5,
},
{
state: 'when array but not of objects',
invalidType: [ 'a', { a: 'b'} ],
},
];
for (const testCase of testCases) {
it(testCase.state, () => {
// arrange
const func = FunctionDataStub
.createWithCall()
.withParametersObject(testCase.invalidType as any);
const expectedError = `parameters must be an array of objects in function(s) "${func.name}"`;
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ func ]);
// assert
expect(act).to.throw(expectedError);
});
}
});
describe('throws when when function have duplicate code', () => {
it('code', () => {
@@ -116,6 +121,37 @@ describe('FunctionsCompiler', () => {
// assert
expect(act).to.throw(expectedError);
});
it('rethrows including function name when FunctionParameter throws', () => {
// arrange
const functionName = 'invalid-function';
const expectedError = `neither "code" or "call" is defined in "${functionName}"`;
const invalidFunction = FunctionDataStub.createWithoutCallOrCodes()
.withName(functionName);
const sut = new MockableFunctionCompiler();
// act
const act = () => sut.compileFunctions([ invalidFunction ]);
// assert
expect(act).to.throw(expectedError);
});
it('rethrows including function name when FunctionParameter throws', () => {
// arrange
const invalidParameterName = 'invalid function p@r4meter name';
const functionName = 'functionName';
let parameterException: Error;
// tslint:disable-next-line:no-unused-expression
try { new FunctionParameter(invalidParameterName, false); } catch (e) { parameterException = e; }
const expectedError = `"${functionName}": ${parameterException.message}`;
const functionData = FunctionDataStub.createWithCode()
.withName(functionName)
.withParameters(new ParameterDefinitionDataStub().withName(invalidParameterName));
// act
const sut = new MockableFunctionCompiler();
const act = () => sut.compileFunctions([ functionData ]);
// assert
expect(act).to.throw(expectedError);
});
});
it('returns empty with empty functions', () => {
// arrange
@@ -136,7 +172,10 @@ describe('FunctionsCompiler', () => {
.withName(name)
.withCode('expected-code')
.withRevertCode('expected-revert-code')
.withParameters('expected-parameter-1', 'expected-parameter-2');
.withParameters(
new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true),
new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false),
);
const sut = new MockableFunctionCompiler();
// act
const collection = sut.compileFunctions([ expected ]);
@@ -188,7 +227,7 @@ describe('FunctionsCompiler', () => {
function expectEqualFunctions(expected: FunctionData, actual: ISharedFunction) {
expect(actual.name).to.equal(expected.name);
expect(actual.parameters).to.deep.equal(expected.parameters);
expect(areScrambledEqual(actual.parameters, expected.parameters));
expectEqualFunctionCode(expected, actual);
}
@@ -197,6 +236,23 @@ function expectEqualFunctionCode(expected: FunctionData, actual: ISharedFunction
expect(actual.revertCode).to.equal(expected.revertCode);
}
function areScrambledEqual(
expected: IReadOnlyFunctionParameterCollection,
actual: readonly ParameterDefinitionData[],
) {
if (expected.all.length !== actual.length) {
return false;
}
for (const expectedParameter of expected.all) {
if (!actual.some(
(a) => a.name === expectedParameter.name
&& (a.optional || false) === expectedParameter.isOptional)) {
return false;
}
}
return true;
}
class MockableFunctionCompiler extends FunctionCompiler {
constructor(functionCallCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub()) {
super(functionCallCompiler);

View File

@@ -0,0 +1,47 @@
import 'mocha';
import { expect } from 'chai';
import { FunctionParameter } from '@/application/Parser/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,47 @@
import 'mocha';
import { expect } from 'chai';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionParameterStub } from '@tests/unit/stubs/FunctionParameterStub';
describe('FunctionParameterCollection', () => {
it('all returns added parameters as expected', () => {
// arrange
const expected = [
new FunctionParameterStub().withName('1'),
new FunctionParameterStub().withName('2').withOptionality(true),
new FunctionParameterStub().withName('3').withOptionality(false),
];
const sut = new FunctionParameterCollection();
for (const parameter of expected) {
sut.addParameter(parameter);
}
// act
const actual = sut.all;
// assert
expect(expected).to.deep.equal(actual);
});
it('throws when function parameters have same names', () => {
// arrange
const parameterName = 'duplicate-parameter';
const expectedError = `duplicate parameter name: "${parameterName}"`;
const sut = new FunctionParameterCollection();
sut.addParameter(new FunctionParameterStub().withName(parameterName));
// act
const act = () =>
sut.addParameter(new FunctionParameterStub().withName(parameterName));
// assert
expect(act).to.throw(expectedError);
});
describe('addParameter', () => {
it('throws if parameter is undefined', () => {
// arrange
const expectedError = 'undefined parameter';
const value = undefined;
const sut = new FunctionParameterCollection();
// act
const act = () => sut.addParameter(value);
// assert
expect(act).to.throw(expectedError);
});
});
});

View File

@@ -1,6 +1,8 @@
import 'mocha';
import { expect } from 'chai';
import { SharedFunction } from '@/application/Parser/Script/Compiler/Function/SharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from '@tests/unit/stubs/FunctionParameterCollectionStub';
describe('SharedFunction', () => {
describe('name', () => {
@@ -31,25 +33,25 @@ describe('SharedFunction', () => {
describe('parameters', () => {
it('sets as expected', () => {
// arrange
const expected = [ 'expected-parameter' ];
const expected = new FunctionParameterCollectionStub()
.withParameterName('test-parameter');
// act
const sut = new SharedFunctionBuilder()
.withParameters(expected)
.build();
// assert
expect(sut.parameters).to.deep.equal(expected);
expect(sut.parameters).to.equal(expected);
});
it('returns empty array if undefined', () => {
it('throws if undefined', () => {
// arrange
const expected = [ ];
const value = undefined;
const expectedError = 'undefined parameters';
const parameters = undefined;
// act
const sut = new SharedFunctionBuilder()
.withParameters(value)
const act = () => new SharedFunctionBuilder()
.withParameters(parameters)
.build();
// assert
expect(sut.parameters).to.not.equal(undefined);
expect(sut.parameters).to.deep.equal(expected);
expect(act).to.throw(expectedError);
});
});
describe('code', () => {
@@ -97,7 +99,7 @@ describe('SharedFunction', () => {
class SharedFunctionBuilder {
private name = 'name';
private parameters: readonly string[] = [ 'parameter' ];
private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private code = 'code';
private revertCode = 'revert-code';
@@ -113,7 +115,7 @@ class SharedFunctionBuilder {
this.name = name;
return this;
}
public withParameters(parameters: readonly string[]) {
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}