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:
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user