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,12 +1,16 @@
import { ExpressionPosition } from './ExpressionPosition';
import { ExpressionArguments, IExpression } from './IExpression';
import { IExpression } from './IExpression';
import { IReadOnlyFunctionCallArgumentCollection } from '../../FunctionCall/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
import { FunctionCallArgumentCollection } from '../../FunctionCall/Argument/FunctionCallArgumentCollection';
export type ExpressionEvaluator = (args?: ExpressionArguments) => string;
export type ExpressionEvaluator = (args: IReadOnlyFunctionCallArgumentCollection) => string;
export class Expression implements IExpression {
constructor(
public readonly position: ExpressionPosition,
public readonly evaluator: ExpressionEvaluator,
public readonly parameters: readonly string[] = new Array<string>()) {
public readonly parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollection()) {
if (!position) {
throw new Error('undefined position');
}
@@ -14,22 +18,42 @@ export class Expression implements IExpression {
throw new Error('undefined evaluator');
}
}
public evaluate(args?: ExpressionArguments): string {
public evaluate(args: IReadOnlyFunctionCallArgumentCollection): string {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
validateThatAllRequiredParametersAreSatisfied(this.parameters, args);
args = filterUnusedArguments(this.parameters, args);
return this.evaluator(args);
}
}
function filterUnusedArguments(
parameters: readonly string[], args: ExpressionArguments): ExpressionArguments {
let result: ExpressionArguments = {};
for (const parameter of Object.keys(args)) {
if (parameters.includes(parameter)) {
result = {
...result,
[parameter]: args[parameter],
};
}
function validateThatAllRequiredParametersAreSatisfied(
parameters: IReadOnlyFunctionParameterCollection,
args: IReadOnlyFunctionCallArgumentCollection,
) {
const requiredParameterNames = parameters
.all
.filter((parameter) => !parameter.isOptional)
.map((parameter) => parameter.name);
const missingParameterNames = requiredParameterNames
.filter((parameterName) => !args.hasArgument(parameterName));
if (missingParameterNames.length) {
throw new Error(
`argument values are provided for required parameters: "${missingParameterNames.join('", "')}"`);
}
return result;
}
function filterUnusedArguments(
parameters: IReadOnlyFunctionParameterCollection,
allFunctionArgs: IReadOnlyFunctionCallArgumentCollection): IReadOnlyFunctionCallArgumentCollection {
const specificCallArgs = new FunctionCallArgumentCollection();
for (const parameter of parameters.all) {
if (parameter.isOptional && !allFunctionArgs.hasArgument(parameter.name)) {
continue; // Optional parameter is not necessarily provided
}
const arg = allFunctionArgs.getArgument(parameter.name);
specificCallArgs.addArgument(arg);
}
return specificCallArgs;
}

View File

@@ -1,12 +1,9 @@
import { ExpressionPosition } from './ExpressionPosition';
import { IReadOnlyFunctionCallArgumentCollection } from '../../FunctionCall/Argument/IFunctionCallArgumentCollection';
import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection';
export interface IExpression {
readonly position: ExpressionPosition;
readonly parameters?: readonly string[];
evaluate(args?: ExpressionArguments): string;
readonly parameters: IReadOnlyFunctionParameterCollection;
evaluate(args: IReadOnlyFunctionCallArgumentCollection): string;
}
export interface ExpressionArguments {
readonly [parameter: string]: string;
}

View File

@@ -1,30 +1,39 @@
import { IExpressionsCompiler, ParameterValueDictionary } from './IExpressionsCompiler';
import { IExpressionsCompiler } from './IExpressionsCompiler';
import { IExpression } from './Expression/IExpression';
import { IExpressionParser } from './Parser/IExpressionParser';
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
import { IReadOnlyFunctionCallArgumentCollection } from '../FunctionCall/Argument/IFunctionCallArgumentCollection';
export class ExpressionsCompiler implements IExpressionsCompiler {
public constructor(private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
public compileExpressions(code: string, parameters?: ParameterValueDictionary): string {
public constructor(
private readonly extractor: IExpressionParser = new CompositeExpressionParser()) { }
public compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string {
if (!args) {
throw new Error('undefined args, send empty collection instead');
}
const expressions = this.extractor.findExpressions(code);
const requiredParameterNames = expressions.map((e) => e.parameters).filter((p) => p).flat();
const uniqueParameterNames = Array.from(new Set(requiredParameterNames));
ensureRequiredArgsProvided(uniqueParameterNames, parameters);
return compileExpressions(expressions, code, parameters);
ensureParamsUsedInCodeHasArgsProvided(expressions, args);
const compiledCode = compileExpressions(expressions, code, args);
return compiledCode;
}
}
function compileExpressions(expressions: IExpression[], code: string, parameters?: ParameterValueDictionary) {
function compileExpressions(
expressions: readonly IExpression[],
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string {
let compiledCode = '';
expressions = expressions
const sortedExpressions = expressions
.slice() // copy the array to not mutate the parameter
.sort((a, b) => b.position.start - a.position.start);
let index = 0;
while (index !== code.length) {
const nextExpression = expressions.pop();
const nextExpression = sortedExpressions.pop();
if (nextExpression) {
compiledCode += code.substring(index, nextExpression.position.start);
const expressionCode = nextExpression.evaluate(parameters);
const expressionCode = nextExpression.evaluate(args);
compiledCode += expressionCode;
index = nextExpression.position.end;
} else {
@@ -35,15 +44,29 @@ function compileExpressions(expressions: IExpression[], code: string, parameters
return compiledCode;
}
function ensureRequiredArgsProvided(parameters: readonly string[], args: ParameterValueDictionary) {
parameters = parameters || [];
args = args || {};
if (!parameters.length) {
function extractRequiredParameterNames(
expressions: readonly IExpression[]): string[] {
const usedParameterNames = expressions
.map((e) => e.parameters.all
.filter((p) => !p.isOptional)
.map((p) => p.name))
.filter((p) => p)
.flat();
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
return uniqueParameterNames;
}
function ensureParamsUsedInCodeHasArgsProvided(
expressions: readonly IExpression[],
providedArgs: IReadOnlyFunctionCallArgumentCollection): void {
const usedParameterNames = extractRequiredParameterNames(expressions);
if (!usedParameterNames?.length) {
return;
}
const notProvidedParameters = parameters.filter((parameter) => !Boolean(args[parameter]));
const notProvidedParameters = usedParameterNames
.filter((parameterName) => !providedArgs.hasArgument(parameterName));
if (notProvidedParameters.length) {
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)}`);
throw new Error(`parameter value(s) not provided for: ${printList(notProvidedParameters)} but used in code`);
}
}

View File

@@ -1,5 +1,7 @@
export interface ParameterValueDictionary { [parameterName: string]: string; }
import { IReadOnlyFunctionCallArgumentCollection } from '../FunctionCall/Argument/IFunctionCallArgumentCollection';
export interface IExpressionsCompiler {
compileExpressions(code: string, parameters?: ParameterValueDictionary): string;
compileExpressions(
code: string,
args: IReadOnlyFunctionCallArgumentCollection): string;
}

View File

@@ -2,13 +2,15 @@ import { IExpression } from '../Expression/IExpression';
import { IExpressionParser } from './IExpressionParser';
import { ParameterSubstitutionParser } from '../SyntaxParsers/ParameterSubstitutionParser';
const parsers = [
const Parsers = [
new ParameterSubstitutionParser(),
];
export class CompositeExpressionParser implements IExpressionParser {
public constructor(private readonly leafs: readonly IExpressionParser[] = parsers) {
if (leafs.some((leaf) => !leaf)) { throw new Error('undefined leaf'); }
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
if (leafs.some((leaf) => !leaf)) {
throw new Error('undefined leaf');
}
}
public findExpressions(code: string): IExpression[] {
const expressions = new Array<IExpression>();

View File

@@ -2,9 +2,12 @@ import { IExpressionParser } from './IExpressionParser';
import { ExpressionPosition } from '../Expression/ExpressionPosition';
import { IExpression } from '../Expression/IExpression';
import { Expression, ExpressionEvaluator } from '../Expression/Expression';
import { IFunctionParameter } from '../../Function/Parameter/IFunctionParameter';
import { FunctionParameterCollection } from '../../Function/Parameter/FunctionParameterCollection';
export abstract class RegexParser implements IExpressionParser {
protected abstract readonly regex: RegExp;
public findExpressions(code: string): IExpression[] {
return Array.from(this.findRegexExpressions(code));
}
@@ -23,7 +26,8 @@ export abstract class RegexParser implements IExpressionParser {
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
}
const primitiveExpression = this.buildExpression(match);
const expression = new Expression(position, primitiveExpression.evaluator, primitiveExpression.parameters);
const parameters = getParameters(primitiveExpression);
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
yield expression;
}
}
@@ -31,5 +35,14 @@ export abstract class RegexParser implements IExpressionParser {
export interface IPrimitiveExpression {
evaluator: ExpressionEvaluator;
parameters?: readonly string[];
parameters?: readonly IFunctionParameter[];
}
function getParameters(
expression: IPrimitiveExpression): FunctionParameterCollection {
const parameters = new FunctionParameterCollection();
for (const parameter of expression.parameters || []) {
parameters.addParameter(parameter);
}
return parameters;
}

View File

@@ -1,12 +1,13 @@
import { RegexParser, IPrimitiveExpression } from '../Parser/RegexParser';
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
export class ParameterSubstitutionParser extends RegexParser {
protected readonly regex = /{{\s*\$([^}| ]+)\s*}}/g;
protected buildExpression(match: RegExpMatchArray): IPrimitiveExpression {
const parameterName = match[1];
return {
parameters: [ parameterName ],
evaluator: (args) => args[parameterName],
parameters: [ new FunctionParameter(parameterName, false) ],
evaluator: (args) => args.getArgument(parameterName).argumentValue,
};
}
}

View File

@@ -5,6 +5,9 @@ import { ISharedFunctionCollection } from './ISharedFunctionCollection';
import { IFunctionCompiler } from './IFunctionCompiler';
import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler';
import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler';
import { FunctionParameter } from './Parameter/FunctionParameter';
import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export class FunctionCompiler implements IFunctionCompiler {
public static readonly instance: IFunctionCompiler = new FunctionCompiler();
@@ -20,20 +23,39 @@ export class FunctionCompiler implements IFunctionCompiler {
functions
.filter((func) => hasCode(func))
.forEach((func) => {
const shared = new SharedFunction(func.name, func.parameters, func.code, func.revertCode);
const parameters = parseParameters(func);
const shared = new SharedFunction(func.name, parameters, func.code, func.revertCode);
collection.addFunction(shared);
});
functions
.filter((func) => hasCall(func))
.forEach((func) => {
const parameters = parseParameters(func);
const code = this.functionCallCompiler.compileCall(func.call, collection);
const shared = new SharedFunction(func.name, func.parameters, code.code, code.revertCode);
const shared = new SharedFunction(func.name, parameters, code.code, code.revertCode);
collection.addFunction(shared);
});
return collection;
}
}
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
const parameters = new FunctionParameterCollection();
if (!data.parameters) {
return parameters;
}
for (const parameterData of data.parameters) {
const isOptional = parameterData.optional || false;
try {
const parameter = new FunctionParameter(parameterData.name, isOptional);
parameters.addParameter(parameter);
} catch (err) {
throw new Error(`"${data.name}": ${err.message}`);
}
}
return parameters;
}
function hasCode(data: FunctionData): boolean {
return Boolean(data.code);
}
@@ -46,10 +68,9 @@ function hasCall(data: FunctionData): boolean {
function ensureValidFunctions(functions: readonly FunctionData[]) {
ensureNoUndefinedItem(functions);
ensureNoDuplicatesInFunctionNames(functions);
ensureNoDuplicatesInParameterNames(functions);
ensureNoDuplicateCode(functions);
ensureEitherCallOrCodeIsDefined(functions);
ensureExpectedParameterNameTypes(functions);
ensureExpectedParametersType(functions);
}
function printList(list: readonly string[]): string {
@@ -69,14 +90,18 @@ function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[])
}
}
function ensureExpectedParameterNameTypes(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions.filter((func) => func.parameters && !isArrayOfStrings(func.parameters));
function ensureExpectedParametersType(functions: readonly FunctionData[]) {
const unexpectedFunctions = functions
.filter((func) => func.parameters && !isArrayOfObjects(func.parameters));
if (unexpectedFunctions.length) {
throw new Error(`unexpected parameter name type in ${printNames(unexpectedFunctions)}`);
const errorMessage = `parameters must be an array of objects in function(s) ${printNames(unexpectedFunctions)}`;
throw new Error(errorMessage);
}
function isArrayOfStrings(value: any): boolean {
return Array.isArray(value) && value.every((item) => typeof item === 'string');
}
}
function isArrayOfObjects(value: any): boolean {
return Array.isArray(value)
&& value.every((item) => typeof item === 'object');
}
function printNames(holders: readonly InstructionHolder[]) {
@@ -90,21 +115,13 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
throw new Error(`duplicate function name: ${printList(duplicateFunctionNames)}`);
}
}
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
if (functions.some((func) => !func)) {
throw new Error(`some functions are undefined`);
}
}
function ensureNoDuplicatesInParameterNames(functions: readonly FunctionData[]) {
const functionsWithParameters = functions
.filter((func) => func.parameters && func.parameters.length > 0);
for (const func of functionsWithParameters) {
const duplicateParameterNames = getDuplicates(func.parameters);
if (duplicateParameterNames.length) {
throw new Error(`"${func.name}": duplicate parameter name: ${printList(duplicateParameterNames)}`);
}
}
}
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
const duplicateCodes = getDuplicates(functions
.map((func) => func.code)

View File

@@ -1,6 +1,8 @@
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export interface ISharedFunction {
readonly name: string;
readonly parameters?: readonly string[];
readonly parameters: IReadOnlyFunctionParameterCollection;
readonly code: string;
readonly revertCode?: string;
}

View File

@@ -0,0 +1,10 @@
import { IFunctionParameter } from './IFunctionParameter';
import { ensureValidParameterName } from '../../ParameterNameValidator';
export class FunctionParameter implements IFunctionParameter {
constructor(
public readonly name: string,
public readonly isOptional: boolean) {
ensureValidParameterName(name);
}
}

View File

@@ -0,0 +1,26 @@
import { IFunctionParameterCollection } from './IFunctionParameterCollection';
import { IFunctionParameter } from './IFunctionParameter';
export class FunctionParameterCollection implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>();
public get all(): readonly IFunctionParameter[] {
return this.parameters;
}
public addParameter(parameter: IFunctionParameter) {
this.ensureValidParameter(parameter);
this.parameters.push(parameter);
}
private includesName(name: string) {
return this.parameters.find((existingParameter) => existingParameter.name === name);
}
private ensureValidParameter(parameter: IFunctionParameter) {
if (!parameter) {
throw new Error('undefined parameter');
}
if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`);
}
}
}

View File

@@ -0,0 +1,4 @@
export interface IFunctionParameter {
readonly name: string;
readonly isOptional: boolean;
}

View File

@@ -0,0 +1,9 @@
import { IFunctionParameter } from './IFunctionParameter';
export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly IFunctionParameter[];
}
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: IFunctionParameter): void;
}

View File

@@ -1,15 +1,15 @@
import { ISharedFunction } from './ISharedFunction';
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
export class SharedFunction implements ISharedFunction {
public readonly parameters: readonly string[];
constructor(
public readonly name: string,
parameters: readonly string[],
public readonly parameters: IReadOnlyFunctionParameterCollection,
public readonly code: string,
public readonly revertCode: string,
public readonly revertCode?: string,
) {
if (!name) { throw new Error('undefined function name'); }
if (!code) { throw new Error(`undefined function ("${name}") code`); }
this.parameters = parameters || [];
if (!parameters) { throw new Error(`undefined parameters`); }
}
}

View File

@@ -0,0 +1,13 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { ensureValidParameterName } from '../../ParameterNameValidator';
export class FunctionCallArgument implements IFunctionCallArgument {
constructor(
public readonly parameterName: string,
public readonly argumentValue: string) {
ensureValidParameterName(parameterName);
if (!argumentValue) {
throw new Error(`undefined argument value for "${parameterName}"`);
}
}
}

View File

@@ -0,0 +1,34 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
import { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
private readonly arguments = new Map<string, IFunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void {
if (!argument) {
throw new Error('undefined argument');
}
if (this.hasArgument(argument.parameterName)) {
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
}
this.arguments.set(argument.parameterName, argument);
}
public getAllParameterNames(): string[] {
return Array.from(this.arguments.keys());
}
public hasArgument(parameterName: string): boolean {
if (!parameterName) {
throw new Error('undefined parameter name');
}
return this.arguments.has(parameterName);
}
public getArgument(parameterName: string): IFunctionCallArgument {
if (!parameterName) {
throw new Error('undefined parameter name');
}
const arg = this.arguments.get(parameterName);
if (!arg) {
throw new Error(`parameter does not exist: ${parameterName}`);
}
return arg;
}
}

View File

@@ -0,0 +1,4 @@
export interface IFunctionCallArgument {
readonly parameterName: string;
readonly argumentValue: string;
}

View File

@@ -0,0 +1,11 @@
import { IFunctionCallArgument } from './IFunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): IFunctionCallArgument;
getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean;
}
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: IFunctionCallArgument): void;
}

View File

@@ -0,0 +1,15 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { IFunctionCall } from './IFunctionCall';
export class FunctionCall implements IFunctionCall {
constructor(
public readonly functionName: string,
public readonly args: IReadOnlyFunctionCallArgumentCollection) {
if (!functionName) {
throw new Error('empty function name in function call');
}
if (!args) {
throw new Error('undefined args');
}
}
}

View File

@@ -1,63 +1,63 @@
import { FunctionCallData, FunctionCallParametersData, FunctionData, ScriptFunctionCallData } from 'js-yaml-loader!@/*';
import { FunctionCallData, ScriptFunctionCallData } from 'js-yaml-loader!@/*';
import { ICompiledCode } from './ICompiledCode';
import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection';
import { IFunctionCallCompiler } from './IFunctionCallCompiler';
import { IExpressionsCompiler } from '../Expressions/IExpressionsCompiler';
import { ExpressionsCompiler } from '../Expressions/ExpressionsCompiler';
import { ISharedFunction } from '../Function/ISharedFunction';
import { IFunctionCall } from './IFunctionCall';
import { FunctionCall } from './FunctionCall';
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
export class FunctionCallCompiler implements IFunctionCallCompiler {
public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler();
protected constructor(
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { }
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) {
}
public compileCall(
call: ScriptFunctionCallData,
functions: ISharedFunctionCollection): ICompiledCode {
if (!functions) { throw new Error('undefined functions'); }
if (!call) { throw new Error('undefined call'); }
const compiledCodes = new Array<ICompiledCode>();
const calls = getCallSequence(call);
calls.forEach((currentCall, currentCallIndex) => {
ensureValidCall(currentCall);
const commonFunction = functions.getFunctionByName(currentCall.function);
ensureExpectedParameters(commonFunction, currentCall);
let functionCode = compileCode(commonFunction, currentCall.parameters, this.expressionsCompiler);
if (currentCallIndex !== calls.length - 1) {
functionCode = appendLine(functionCode);
}
compiledCodes.push(functionCode);
});
const compiledCode = merge(compiledCodes);
return compiledCode;
const compiledFunctions = new Array<ICompiledFunction>();
const callSequence = getCallSequence(call);
for (const currentCall of callSequence) {
const functionCall = parseFunctionCall(currentCall);
const sharedFunction = functions.getFunctionByName(functionCall.functionName);
ensureThatCallArgumentsExistInParameterDefinition(sharedFunction, functionCall.args);
const compiledFunction = compileCode(sharedFunction, functionCall.args, this.expressionsCompiler);
compiledFunctions.push(compiledFunction);
}
return {
code: merge(compiledFunctions.map((f) => f.code)),
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
};
}
}
function ensureExpectedParameters(func: FunctionData, call: FunctionCallData) {
const actual = Object.keys(call.parameters || {});
const expected = func.parameters || [];
if (!actual.length && !expected.length) {
return;
}
const unexpectedParameters = actual.filter((callParam) => !expected.includes(callParam));
if (unexpectedParameters.length) {
throw new Error(
`function "${func.name}" has unexpected parameter(s) provided: "${unexpectedParameters.join('", "')}"`);
}
function merge(codeParts: readonly string[]): string {
return codeParts
.filter((part) => part?.length > 0)
.join('\n');
}
function merge(codes: readonly ICompiledCode[]): ICompiledCode {
return {
code: codes.map((code) => code.code).join(''),
revertCode: codes.map((code) => code.revertCode).join(''),
};
interface ICompiledFunction {
readonly code: string;
readonly revertCode: string;
}
function compileCode(
func: FunctionData,
parameters: FunctionCallParametersData,
compiler: IExpressionsCompiler): ICompiledCode {
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection,
compiler: IExpressionsCompiler): ICompiledFunction {
return {
code: compiler.compileExpressions(func.code, parameters),
revertCode: compiler.compileExpressions(func.revertCode, parameters),
code: compiler.compileExpressions(func.code, args),
revertCode: compiler.compileExpressions(func.revertCode, args),
};
}
@@ -71,19 +71,31 @@ function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] {
return [ call as FunctionCallData ];
}
function ensureValidCall(call: FunctionCallData) {
function parseFunctionCall(call: FunctionCallData): IFunctionCall {
if (!call) {
throw new Error(`undefined function call`);
}
if (!call.function) {
throw new Error(`empty function name called`);
const args = new FunctionCallArgumentCollection();
for (const parameterName of Object.keys(call.parameters || {})) {
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
args.addArgument(arg);
}
return new FunctionCall(call.function, args);
}
function appendLine(code: ICompiledCode): ICompiledCode {
const appendLineIfNotEmpty = (str: string) => str ? `${str}\n` : str;
return {
code: appendLineIfNotEmpty(code.code),
revertCode: appendLineIfNotEmpty(code.revertCode),
};
function ensureThatCallArgumentsExistInParameterDefinition(
func: ISharedFunction,
args: IReadOnlyFunctionCallArgumentCollection): void {
const callArgumentNames = args.getAllParameterNames();
const functionParameterNames = func.parameters.all.map((param) => param.name) || [];
if (!callArgumentNames.length && !functionParameterNames.length) {
return;
}
const parametersOutsideFunction = callArgumentNames
.filter((callParam) => !functionParameterNames.includes(callParam));
if (parametersOutsideFunction.length) {
throw new Error(
`function "${func.name}" has unexpected parameter(s) provided:` +
`"${parametersOutsideFunction.join('", "')}"`);
}
}

View File

@@ -0,0 +1,6 @@
import { IReadOnlyFunctionCallArgumentCollection } from './Argument/IFunctionCallArgumentCollection';
export interface IFunctionCall {
readonly functionName: string;
readonly args: IReadOnlyFunctionCallArgumentCollection;
}

View File

@@ -0,0 +1,8 @@
export function ensureValidParameterName(parameterName: string) {
if (!parameterName) {
throw new Error('undefined parameter name');
}
if (!parameterName.match(/^[0-9a-zA-Z]+$/)) {
throw new Error(`parameter name must be alphanumeric but it was "${parameterName}"`);
}
}