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