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,15 +1,23 @@
import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition';
import { ExpressionArguments, IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
import { IExpression } from '@/application/Parser/Script/Compiler/Expressions/Expression/IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
export class ExpressionStub implements IExpression {
public callHistory = new Array<ExpressionArguments>();
public callHistory = new Array<IReadOnlyFunctionCallArgumentCollection>();
public position = new ExpressionPosition(0, 5);
public parameters = [];
public parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub();
private result: string;
public withParameters(...parameters: string[]) {
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}
public withParameterNames(parameterNames: readonly string[], isOptional = false) {
const collection = new FunctionParameterCollectionStub()
.withParameterNames(parameterNames, isOptional);
return this.withParameters(collection);
}
public withPosition(start: number, end: number) {
this.position = new ExpressionPosition(start, end);
return this;
@@ -18,7 +26,7 @@ export class ExpressionStub implements IExpression {
this.result = result;
return this;
}
public evaluate(args?: ExpressionArguments): string {
public evaluate(args: IReadOnlyFunctionCallArgumentCollection): string {
this.callHistory.push(args);
const result = this.result || `[expression-stub] args: ${args ? Object.keys(args).map((key) => `${key}: ${args[key]}`).join('", "') : 'none'}`;
return result;

View File

@@ -1,30 +1,56 @@
import { IExpressionsCompiler, ParameterValueDictionary } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection';
import { scrambledEqual } from '@/application/Common/Array';
interface Scenario { code: string; parameters: ParameterValueDictionary; result: string; }
export class ExpressionsCompilerStub implements IExpressionsCompiler {
public readonly callHistory = new Array<{code: string, parameters?: ParameterValueDictionary}>();
private readonly scenarios = new Array<Scenario>();
public setup(code: string, parameters: ParameterValueDictionary, result: string) {
public readonly callHistory = new Array<{code: string, parameters: IReadOnlyFunctionCallArgumentCollection}>();
private readonly scenarios = new Array<ITestScenario>();
public setup(
code: string,
parameters: IReadOnlyFunctionCallArgumentCollection,
result: string): ExpressionsCompilerStub {
this.scenarios.push({ code, parameters, result });
return this;
}
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
public compileExpressions(
code: string,
parameters: IReadOnlyFunctionCallArgumentCollection): string {
this.callHistory.push({ code, parameters});
const scenario = this.scenarios.find((s) => s.code === code && deepEqual(s.parameters, parameters));
if (scenario) {
return scenario.result;
}
return `[ExpressionsCompilerStub] code: "${code}"` +
`| parameters: ${Object.keys(parameters || {}).map((p) => p + '=' + parameters[p]).join(',')}`;
const parametersAndValues = parameters
.getAllParameterNames()
.map((name) => `${name}=${parameters.getArgument(name).argumentValue}`)
.join('", "');
return `[ExpressionsCompilerStub] code: "${code}" | parameters: "${parametersAndValues}"`;
}
}
function deepEqual(dict1: ParameterValueDictionary, dict2: ParameterValueDictionary) {
const dict1Keys = Object.keys(dict1 || {});
const dict2Keys = Object.keys(dict2 || {});
if (dict1Keys.length !== dict2Keys.length) {
interface ITestScenario {
readonly code: string;
readonly parameters: IReadOnlyFunctionCallArgumentCollection;
readonly result: string;
}
function deepEqual(
expected: IReadOnlyFunctionCallArgumentCollection,
actual: IReadOnlyFunctionCallArgumentCollection): boolean {
const expectedParameterNames = expected.getAllParameterNames();
const actualParameterNames = actual.getAllParameterNames();
if (!scrambledEqual(expectedParameterNames, actualParameterNames)) {
return false;
}
return dict1Keys.every((key) => dict2.hasOwnProperty(key) && dict2[key] === dict1[key]);
for (const parameterName of expectedParameterNames) {
const expectedValue = expected.getArgument(parameterName);
const actualValue = expected.getArgument(parameterName);
if (expectedValue !== actualValue) {
return false;
}
}
return true;
}

View File

@@ -0,0 +1,39 @@
// tslint:disable-next-line:max-line-length
import { IFunctionCallArgument } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgument';
import { IFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentStub } from './FunctionCallArgumentStub';
export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection {
private args = new Array<IFunctionCallArgument>();
public withArgument(parameterName: string, argumentValue: string) {
const arg = new FunctionCallArgumentStub()
.withParameterName(parameterName)
.withArgumentValue(argumentValue);
this.addArgument(arg);
return this;
}
public withArguments(args: { readonly [index: string]: string }) {
for (const parameterName of Object.keys(args)) {
const parameterValue = args[parameterName];
this.withArgument(parameterName, parameterValue);
}
return this;
}
public hasArgument(parameterName: string): boolean {
return this.args.some((a) => a.parameterName === parameterName);
}
public addArgument(argument: IFunctionCallArgument): void {
this.args.push(argument);
}
public getAllParameterNames(): string[] {
return this.args.map((a) => a.parameterName);
}
public getArgument(parameterName: string): IFunctionCallArgument {
const arg = this.args.find((a) => a.parameterName === parameterName);
if (!arg) {
throw new Error(`no argument exists for parameter "${parameterName}"`);
}
return arg;
}
}

View File

@@ -0,0 +1,15 @@
// tslint:disable-next-line:max-line-length
import { IFunctionCallArgument } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgument';
export class FunctionCallArgumentStub implements IFunctionCallArgument {
public parameterName = 'stub-parameter-name';
public argumentValue = 'stub-arg-name';
public withParameterName(parameterName: string) {
this.parameterName = parameterName;
return this;
}
public withArgumentValue(argumentValue: string) {
this.argumentValue = argumentValue;
return this;
}
}

View File

@@ -1,4 +1,4 @@
import { FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!@/*';
import { FunctionData, ParameterDefinitionData, ScriptFunctionCallData } from 'js-yaml-loader!@/*';
export class FunctionDataStub implements FunctionData {
public static createWithCode() {
@@ -19,11 +19,11 @@ export class FunctionDataStub implements FunctionData {
return new FunctionDataStub();
}
public name = 'function data stub';
public name = 'functionDataStub';
public code: string;
public revertCode: string;
public parameters?: readonly string[];
public call?: ScriptFunctionCallData;
public parameters?: readonly ParameterDefinitionData[];
private constructor() { }
@@ -31,7 +31,10 @@ export class FunctionDataStub implements FunctionData {
this.name = name;
return this;
}
public withParameters(...parameters: string[]) {
public withParameters(...parameters: readonly ParameterDefinitionData[]) {
return this.withParametersObject(parameters);
}
public withParametersObject(parameters: readonly ParameterDefinitionData[]) {
this.parameters = parameters;
return this;
}

View File

@@ -0,0 +1,27 @@
import { IFunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameter';
import { IFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterStub } from './FunctionParameterStub';
export class FunctionParameterCollectionStub implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>();
public addParameter(parameter: IFunctionParameter): void {
this.parameters.push(parameter);
}
public get all(): readonly IFunctionParameter[] {
return this.parameters;
}
public withParameterName(parameterName: string, isOptional = true) {
const parameter = new FunctionParameterStub()
.withName(parameterName)
.withOptionality(isOptional);
this.addParameter(parameter);
return this;
}
public withParameterNames(parameterNames: readonly string[], isOptional = true) {
for (const parameterName of parameterNames) {
this.withParameterName(parameterName, isOptional);
}
return this;
}
}

View File

@@ -0,0 +1,14 @@
import { IFunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameter';
export class FunctionParameterStub implements IFunctionParameter {
public name: string = 'function-parameter-stub';
public isOptional: boolean = true;
public withName(name: string) {
this.name = name;
return this;
}
public withOptionality(isOptional: boolean) {
this.isOptional = isOptional;
return this;
}
}

View File

@@ -0,0 +1,14 @@
import { ParameterDefinitionData } from 'js-yaml-loader!@/*';
export class ParameterDefinitionDataStub implements ParameterDefinitionData {
public name: string;
public optional?: boolean;
public withName(name: string) {
this.name = name;
return this;
}
public withOptionality(isOptional: boolean) {
this.optional = isOptional;
return this;
}
}

View File

@@ -1,5 +1,6 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
import { SharedFunctionStub } from './SharedFunctionStub';
export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
private readonly functions = new Map<string, ISharedFunction>();
@@ -11,11 +12,9 @@ export class SharedFunctionCollectionStub implements ISharedFunctionCollection {
if (this.functions.has(name)) {
return this.functions.get(name);
}
return {
name,
parameters: [],
code: 'code by SharedFunctionCollectionStub',
revertCode: 'revert-code by SharedFunctionCollectionStub',
};
return new SharedFunctionStub()
.withName(name)
.withCode('code by SharedFunctionCollectionStub')
.withRevertCode('revert-code by SharedFunctionCollectionStub');
}
}

View File

@@ -1,10 +1,11 @@
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub';
export class SharedFunctionStub implements ISharedFunction {
public name = 'shared-function-stub-name';
public parameters?: readonly string[] = [
'shared-function-stub-parameter',
];
public parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub()
.withParameterName('shared-function-stub-parameter-name');
public code = 'shared-function-stub-code';
public revertCode = 'shared-function-stub-revert-code';
@@ -20,8 +21,15 @@ export class SharedFunctionStub implements ISharedFunction {
this.revertCode = revertCode;
return this;
}
public withParameters(...params: string[]) {
this.parameters = params;
public withParameters(parameters: IReadOnlyFunctionParameterCollection) {
this.parameters = parameters;
return this;
}
public withParameterNames(...parameterNames: readonly string[]) {
let collection = new FunctionParameterCollectionStub();
for (const name of parameterNames) {
collection = collection.withParameterName(name);
}
return this.withParameters(collection);
}
}