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