diff --git a/docs/collection-files.md b/docs/collection-files.md index 55ca8d2a..662f1f8a 100644 --- a/docs/collection-files.md +++ b/docs/collection-files.md @@ -45,9 +45,11 @@ ### `Script` - Script represents a single tweak. -- A script must include either: - - A `code` and `revertCode` - - Or `call` to call YAML-defined functions +- A script can be of two different types (just like [functions](#function)): + 1. Inline script; a script with an inline code + - Must define `code` property and optionally `revertCode` but not `call` + 2. Caller script; a script that calls other functions + - Must define `call` property but not `code` or `revertCode` - 🙏 For any new script, please add `revertCode` and `docs` values if possible. #### `Script` syntax @@ -98,12 +100,18 @@ appName: Microsoft.WindowsFeedbackHub ``` + - 💡 [Expressions (templating)](./templating.md#expressions) can be used as parameter value + ### `Function` -- Functions allow re-usable code throughout the defined scripts -- Functions are templates compiled by privacy.sexy and uses special expression expressions -- Functions can call other functions by defining `call` property instead of `code` -- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution) +- Functions allow re-usable code throughout the defined scripts. +- Functions are templates compiled by privacy.sexy and uses special expression expressions. +- A function can be of two different types (just like [scripts](#script)): + 1. Inline function: a function with an inline code. + - Must define `code` property and optionally `revertCode` but not `call`. + 2. Caller function: a function that calls other functions. + - Must define `call` property but not `code` or `revertCode`. +- 👀 Read more on [Templating](./templating.md) for function expressions and [example usages](./templating.md#parameter-substitution). #### `Function` syntax @@ -114,18 +122,20 @@ - ❗ Function names must be unique - `parameters`: `[` ***[`FunctionParameter`](#FunctionParameter)*** `, ... ]` - List of parameters that function code refers to. - - ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions](./templating.md#expressions) + - ❗ Must be defined to be able use in [`FunctionCall`](#functioncall) or [expressions (templating)](./templating.md#expressions) `code`: *`string`* (**required** if `call` is undefined) - Batch file commands that will be executed + - 💡 [Expressions (templating)](./templating.md#expressions) can be used in its value - 💡 If defined, best practice to also define `revertCode` - ❗ If not defined `call` must be defined - `revertCode`: *`string`* - Code that'll undo the change done by `code` property. - E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1` - then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0` + - 💡 [Expressions (templating)](./templating.md#expressions) can be used in code - `call`: ***[`FunctionCall`](#FunctionCall)*** | `[` ***[`FunctionCall`](#FunctionCall)*** `, ... ]` (may be **required**) - A shared function or sequence of functions to call (called in order) - - The parameter values that are sent can use [expressions](./templating.md#expressions) + - The parameter values that are sent can use [expressions (templating)](./templating.md#expressions) - ❗ If not defined `code` must be defined ### `FunctionParameter` @@ -137,7 +147,7 @@ - `name`: *`string`* (**required**) - Name of the parameters that the function has. - - Parameter names must be defined to be used in [expressions](./templating.md#expressions). + - Parameter names must be defined to be used in [expressions (templating)](./templating.md#expressions). - ❗ Parameter names must be unique and include alphanumeric characters only. - `optional`: *`boolean`* (default: `false`) - Specifies whether the caller [Script](#script) must provide any value for the parameter. diff --git a/docs/templating.md b/docs/templating.md index 351f8b05..7f7eef4d 100644 --- a/docs/templating.md +++ b/docs/templating.md @@ -10,8 +10,9 @@ - Expressions in the language are defined inside mustaches (double brackets, `{{` and `}}`). - Expression syntax is inspired mainly by [Go Templates](https://pkg.go.dev/text/template). - -## Syntax +- Expressions are used in and enabled by functions where they can be used. + - In script definition parts of a function, see [`Function`](./collection-files.md#Function). + - When doing a call as argument values, see [`FunctionCall`](./collection-files.md#Function). ### Parameter substitution diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts index 281276b6..51660d28 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/Expression.ts @@ -1,9 +1,9 @@ import { ExpressionPosition } from './ExpressionPosition'; import { IExpression } from './IExpression'; -import { IReadOnlyFunctionCallArgumentCollection } from '../../FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection'; import { IReadOnlyFunctionParameterCollection } from '../../Function/Parameter/IFunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; -import { FunctionCallArgumentCollection } from '../../FunctionCall/Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection'; import { IExpressionEvaluationContext } from './ExpressionEvaluationContext'; import { ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; diff --git a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts index 7b5f6700..42701251 100644 --- a/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts +++ b/src/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.ts @@ -1,4 +1,4 @@ -import { IReadOnlyFunctionCallArgumentCollection } from '../../FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '../../Function/Call/Argument/IFunctionCallArgumentCollection'; import { IPipelineCompiler } from '../Pipes/IPipelineCompiler'; import { PipelineCompiler } from '../Pipes/PipelineCompiler'; diff --git a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts index 6f852f4a..a4898f47 100644 --- a/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler.ts @@ -2,7 +2,7 @@ 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'; +import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; import { ExpressionEvaluationContext } from './Expression/ExpressionEvaluationContext'; import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; diff --git a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts index c0c4632f..f73e8471 100644 --- a/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts +++ b/src/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler.ts @@ -1,4 +1,4 @@ -import { IReadOnlyFunctionCallArgumentCollection } from '../FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection'; export interface IExpressionsCompiler { compileExpressions( diff --git a/src/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts similarity index 84% rename from src/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.ts rename to src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts index 5bed91f8..dabfb55f 100644 --- a/src/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.ts +++ b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts @@ -1,5 +1,5 @@ import { IFunctionCallArgument } from './IFunctionCallArgument'; -import { ensureValidParameterName } from '../../ParameterNameValidator'; +import { ensureValidParameterName } from '../../Shared/ParameterNameValidator'; export class FunctionCallArgument implements IFunctionCallArgument { constructor( diff --git a/src/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection.ts rename to src/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.ts diff --git a/src/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgument.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgument.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgument.ts rename to src/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgument.ts diff --git a/src/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection.ts b/src/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection.ts rename to src/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection.ts diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts new file mode 100644 index 00000000..e6bfc3a7 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.ts @@ -0,0 +1,139 @@ +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import { ICompiledCode } from './ICompiledCode'; +import { ISharedFunctionCollection } from '../../ISharedFunctionCollection'; +import { IFunctionCallCompiler } from './IFunctionCallCompiler'; +import { IExpressionsCompiler } from '../../../Expressions/IExpressionsCompiler'; +import { ExpressionsCompiler } from '../../../Expressions/ExpressionsCompiler'; +import { ISharedFunction, IFunctionCode } from '../../ISharedFunction'; +import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCall } from '../FunctionCall'; +import { FunctionCallArgumentCollection } from '../Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; + +export class FunctionCallCompiler implements IFunctionCallCompiler { + public static readonly instance: IFunctionCallCompiler = new FunctionCallCompiler(); + + protected constructor( + private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler()) { + } + + public compileCall( + calls: IFunctionCall[], + functions: ISharedFunctionCollection): ICompiledCode { + if (!functions) { throw new Error('undefined functions'); } + if (!calls) { throw new Error('undefined calls'); } + if (calls.some((f) => !f)) { throw new Error('undefined function call'); } + const context: ICompilationContext = { + allFunctions: functions, + callSequence: calls, + expressionsCompiler: this.expressionsCompiler, + }; + const code = compileCallSequence(context); + return code; + } +} + +interface ICompilationContext { + allFunctions: ISharedFunctionCollection; + callSequence: readonly IFunctionCall[]; + expressionsCompiler: IExpressionsCompiler; +} + +interface ICompiledFunctionCall { + readonly code: string; + readonly revertCode: string; +} + +function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall { + const compiledFunctions = new Array(); + for (const call of context.callSequence) { + const compiledCode = compileSingleCall(call, context); + compiledFunctions.push(...compiledCode); + } + return { + code: merge(compiledFunctions.map((f) => f.code)), + revertCode: merge(compiledFunctions.map((f) => f.revertCode)), + }; +} + +function compileSingleCall(call: IFunctionCall, context: ICompilationContext): ICompiledFunctionCall[] { + const func = context.allFunctions.getFunctionByName(call.functionName); + ensureThatCallArgumentsExistInParameterDefinition(func, call.args); + if (func.body.code) { // Function with inline code + const compiledCode = compileCode(func.body.code, call.args, context.expressionsCompiler); + return [ compiledCode ]; + } else { // Function with inner calls + return func.body.calls + .map((innerCall) => { + const compiledArgs = compileArgs(innerCall.args, call.args, context.expressionsCompiler); + const compiledCall = new FunctionCall(innerCall.functionName, compiledArgs); + return compileSingleCall(compiledCall, context); + }) + .flat(); + } +} + +function compileCode( + code: IFunctionCode, + args: IReadOnlyFunctionCallArgumentCollection, + compiler: IExpressionsCompiler): ICompiledFunctionCall { + return { + code: compiler.compileExpressions(code.do, args), + revertCode: compiler.compileExpressions(code.revert, args), + }; +} + +function compileArgs( + argsToCompile: IReadOnlyFunctionCallArgumentCollection, + args: IReadOnlyFunctionCallArgumentCollection, + compiler: IExpressionsCompiler, + ): IReadOnlyFunctionCallArgumentCollection { + const compiledArgs = new FunctionCallArgumentCollection(); + for (const parameterName of argsToCompile.getAllParameterNames()) { + const argumentValue = argsToCompile.getArgument(parameterName).argumentValue; + const compiledValue = compiler.compileExpressions(argumentValue, args); + const newArgument = new FunctionCallArgument(parameterName, compiledValue); + compiledArgs.addArgument(newArgument); + } + return compiledArgs; +} + +function merge(codeParts: readonly string[]): string { + return codeParts + .filter((part) => part?.length > 0) + .join('\n'); +} + +function ensureThatCallArgumentsExistInParameterDefinition( + func: ISharedFunction, + args: IReadOnlyFunctionCallArgumentCollection): void { + const callArgumentNames = args.getAllParameterNames(); + const functionParameterNames = func.parameters.all.map((param) => param.name) || []; + const unexpectedParameters = findUnexpectedParameters(callArgumentNames, functionParameterNames); + throwIfNotEmpty(func.name, unexpectedParameters, functionParameterNames); +} + +function findUnexpectedParameters( + callArgumentNames: string[], + functionParameterNames: string[]): string[] { + if (!callArgumentNames.length && !functionParameterNames.length) { + return []; + } + return callArgumentNames + .filter((callParam) => !functionParameterNames.includes(callParam)); +} + +function throwIfNotEmpty( + functionName: string, + unexpectedParameters: string[], + expectedParameters: string[]) { + if (!unexpectedParameters.length) { + return; + } + throw new Error( + `Function "${functionName}" has unexpected parameter(s) provided: ` + + `"${unexpectedParameters.join('", "')}"` + + '. Expected parameter(s): ' + + (expectedParameters.length ? `"${expectedParameters.join('", "')}"` : 'none'), + ); +} diff --git a/src/application/Parser/Script/Compiler/FunctionCall/ICompiledCode.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/ICompiledCode.ts rename to src/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode.ts diff --git a/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts new file mode 100644 index 00000000..c0d49576 --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler.ts @@ -0,0 +1,9 @@ +import { ICompiledCode } from './ICompiledCode'; +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { IFunctionCall } from '../IFunctionCall'; + +export interface IFunctionCallCompiler { + compileCall( + calls: IFunctionCall[], + functions: ISharedFunctionCollection): ICompiledCode; +} diff --git a/src/application/Parser/Script/Compiler/FunctionCall/FunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/FunctionCall.ts rename to src/application/Parser/Script/Compiler/Function/Call/FunctionCall.ts diff --git a/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts new file mode 100644 index 00000000..a2cb02ae --- /dev/null +++ b/src/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.ts @@ -0,0 +1,35 @@ +import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*'; +import { IFunctionCall } from './IFunctionCall'; +import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgument } from './Argument/FunctionCallArgument'; +import { FunctionCall } from './FunctionCall'; + +export function parseFunctionCalls(calls: FunctionCallsData): IFunctionCall[] { + if (!calls) { + throw new Error('undefined call data'); + } + const sequence = getCallSequence(calls); + return sequence.map((call) => parseFunctionCall(call)); +} + +function getCallSequence(calls: FunctionCallsData): FunctionCallData[] { + if (typeof calls !== 'object') { + throw new Error('called function(s) must be an object'); + } + if (calls instanceof Array) { + return calls as FunctionCallData[]; + } + return [ calls as FunctionCallData ]; +} + +function parseFunctionCall(call: FunctionCallData): IFunctionCall { + if (!call) { + throw new Error(`undefined function call`); + } + 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); +} diff --git a/src/application/Parser/Script/Compiler/FunctionCall/IFunctionCall.ts b/src/application/Parser/Script/Compiler/Function/Call/IFunctionCall.ts similarity index 100% rename from src/application/Parser/Script/Compiler/FunctionCall/IFunctionCall.ts rename to src/application/Parser/Script/Compiler/Function/Call/IFunctionCall.ts diff --git a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts index f23da1f2..f577587a 100644 --- a/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunction.ts @@ -1,8 +1,24 @@ import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; +import { IFunctionCall } from '../Function/Call/IFunctionCall'; export interface ISharedFunction { readonly name: string; readonly parameters: IReadOnlyFunctionParameterCollection; - readonly code: string; - readonly revertCode?: string; + readonly body: ISharedFunctionBody; +} + +export interface ISharedFunctionBody { + readonly type: FunctionBodyType; + readonly code: IFunctionCode; + readonly calls: readonly IFunctionCall[]; +} + +export enum FunctionBodyType { + Code, + Calls, +} + +export interface IFunctionCode { + readonly do: string; + readonly revert?: string; } diff --git a/src/application/Parser/Script/Compiler/Function/IFunctionCompiler.ts b/src/application/Parser/Script/Compiler/Function/ISharedFunctionsParser.ts similarity index 50% rename from src/application/Parser/Script/Compiler/Function/IFunctionCompiler.ts rename to src/application/Parser/Script/Compiler/Function/ISharedFunctionsParser.ts index 27558bbc..99350c2e 100644 --- a/src/application/Parser/Script/Compiler/Function/IFunctionCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/ISharedFunctionsParser.ts @@ -1,6 +1,6 @@ import { FunctionData } from 'js-yaml-loader!@/*'; import { ISharedFunctionCollection } from './ISharedFunctionCollection'; -export interface IFunctionCompiler { - compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection; +export interface ISharedFunctionsParser { + parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection; } diff --git a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter.ts b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter.ts index f6f49da2..5b59f794 100644 --- a/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter.ts +++ b/src/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter.ts @@ -1,5 +1,5 @@ import { IFunctionParameter } from './IFunctionParameter'; -import { ensureValidParameterName } from '../../ParameterNameValidator'; +import { ensureValidParameterName } from '../Shared/ParameterNameValidator'; export class FunctionParameter implements IFunctionParameter { constructor( diff --git a/src/application/Parser/Script/Compiler/ParameterNameValidator.ts b/src/application/Parser/Script/Compiler/Function/Shared/ParameterNameValidator.ts similarity index 100% rename from src/application/Parser/Script/Compiler/ParameterNameValidator.ts rename to src/application/Parser/Script/Compiler/Function/Shared/ParameterNameValidator.ts diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts index 33678ebb..b9cbc514 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunction.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunction.ts @@ -1,15 +1,49 @@ -import { ISharedFunction } from './ISharedFunction'; +import { IFunctionCall } from '../Function/Call/IFunctionCall'; +import { FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody } from './ISharedFunction'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; -export class SharedFunction implements ISharedFunction { +export function createCallerFunction( + name: string, + parameters: IReadOnlyFunctionParameterCollection, + callSequence: readonly IFunctionCall[]): ISharedFunction { + if (!callSequence) { + throw new Error(`undefined call sequence in function "${name}"`); + } + if (!callSequence.length) { + throw new Error(`empty call sequence in function "${name}"`); + } + return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls); +} + +export function createFunctionWithInlineCode( + name: string, + parameters: IReadOnlyFunctionParameterCollection, + code: string, + revertCode?: string): ISharedFunction { + if (!code) { + throw new Error(`undefined code in function "${name}"`); + } + const content: IFunctionCode = { + do: code, + revert: revertCode, + }; + return new SharedFunction(name, parameters, content, FunctionBodyType.Code); +} + +class SharedFunction implements ISharedFunction { + public readonly body: ISharedFunctionBody; constructor( public readonly name: string, public readonly parameters: IReadOnlyFunctionParameterCollection, - public readonly code: string, - public readonly revertCode?: string, + content: IFunctionCode | readonly IFunctionCall[], + bodyType: FunctionBodyType, ) { if (!name) { throw new Error('undefined function name'); } - if (!code) { throw new Error(`undefined function ("${name}") code`); } if (!parameters) { throw new Error(`undefined parameters`); } + this.body = { + type: bodyType, + code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined, + calls: bodyType === FunctionBodyType.Calls ? content as readonly IFunctionCall[] : undefined, + }; } } diff --git a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts index 1cae1436..f457da4f 100644 --- a/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionCollection.ts @@ -6,7 +6,7 @@ export class SharedFunctionCollection implements ISharedFunctionCollection { public addFunction(func: ISharedFunction): void { if (!func) { throw new Error('undefined function'); } - if (this.functionsByName.has(func.name)) { + if (this.has(func.name)) { throw new Error(`function with name ${func.name} already exists`); } this.functionsByName.set(func.name, func); @@ -20,4 +20,8 @@ export class SharedFunctionCollection implements ISharedFunctionCollection { } return func; } + + private has(functionName: string) { + return this.functionsByName.has(functionName); + } } diff --git a/src/application/Parser/Script/Compiler/Function/FunctionCompiler.ts b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts similarity index 76% rename from src/application/Parser/Script/Compiler/Function/FunctionCompiler.ts rename to src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts index fd4f3ef6..381336bf 100644 --- a/src/application/Parser/Script/Compiler/Function/FunctionCompiler.ts +++ b/src/application/Parser/Script/Compiler/Function/SharedFunctionsParser.ts @@ -1,44 +1,42 @@ import { FunctionData, InstructionHolder } from 'js-yaml-loader!@/*'; -import { SharedFunction } from './SharedFunction'; +import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { SharedFunctionCollection } from './SharedFunctionCollection'; import { ISharedFunctionCollection } from './ISharedFunctionCollection'; -import { IFunctionCompiler } from './IFunctionCompiler'; -import { IFunctionCallCompiler } from '../FunctionCall/IFunctionCallCompiler'; -import { FunctionCallCompiler } from '../FunctionCall/FunctionCallCompiler'; +import { ISharedFunctionsParser } from './ISharedFunctionsParser'; import { FunctionParameter } from './Parameter/FunctionParameter'; import { FunctionParameterCollection } from './Parameter/FunctionParameterCollection'; import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; +import { ISharedFunction } from './ISharedFunction'; +import { parseFunctionCalls } from './Call/FunctionCallParser'; -export class FunctionCompiler implements IFunctionCompiler { - public static readonly instance: IFunctionCompiler = new FunctionCompiler(); - protected constructor( - private readonly functionCallCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance) { - } - public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection { +export class SharedFunctionsParser implements ISharedFunctionsParser { + public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser(); + public parseFunctions( + functions: readonly FunctionData[]): ISharedFunctionCollection { const collection = new SharedFunctionCollection(); if (!functions || !functions.length) { return collection; } ensureValidFunctions(functions); - functions - .filter((func) => hasCode(func)) - .forEach((func) => { - 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, parameters, code.code, code.revertCode); - collection.addFunction(shared); - }); + for (const func of functions) { + const sharedFunction = parseFunction(func); + collection.addFunction(sharedFunction); + } return collection; } } +function parseFunction(data: FunctionData): ISharedFunction { + const name = data.name; + const parameters = parseParameters(data); + if (hasCode(data)) { + return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); + } else { // has call + const calls = parseFunctionCalls(data.call); + return createCallerFunction(name, parameters, calls); + } +} + function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection { const parameters = new FunctionParameterCollection(); if (!data.parameters) { @@ -64,7 +62,6 @@ function hasCall(data: FunctionData): boolean { return Boolean(data.call); } - function ensureValidFunctions(functions: readonly FunctionData[]) { ensureNoUndefinedItem(functions); ensureNoDuplicatesInFunctionNames(functions); diff --git a/src/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.ts deleted file mode 100644 index 2fd6a1c3..00000000 --- a/src/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.ts +++ /dev/null @@ -1,101 +0,0 @@ -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()) { - - } - - public compileCall( - call: ScriptFunctionCallData, - functions: ISharedFunctionCollection): ICompiledCode { - if (!functions) { throw new Error('undefined functions'); } - if (!call) { throw new Error('undefined call'); } - const compiledFunctions = new Array(); - 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 merge(codeParts: readonly string[]): string { - return codeParts - .filter((part) => part?.length > 0) - .join('\n'); -} - -interface ICompiledFunction { - readonly code: string; - readonly revertCode: string; -} - -function compileCode( - func: ISharedFunction, - args: IReadOnlyFunctionCallArgumentCollection, - compiler: IExpressionsCompiler): ICompiledFunction { - return { - code: compiler.compileExpressions(func.code, args), - revertCode: compiler.compileExpressions(func.revertCode, args), - }; -} - -function getCallSequence(call: ScriptFunctionCallData): FunctionCallData[] { - if (typeof call !== 'object') { - throw new Error('called function(s) must be an object'); - } - if (call instanceof Array) { - return call as FunctionCallData[]; - } - return [ call as FunctionCallData ]; -} - -function parseFunctionCall(call: FunctionCallData): IFunctionCall { - if (!call) { - throw new Error(`undefined function call`); - } - 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 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('", "')}"`); - } -} diff --git a/src/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler.ts b/src/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler.ts deleted file mode 100644 index 2fb0fac5..00000000 --- a/src/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ScriptFunctionCallData } from 'js-yaml-loader!@/*'; -import { ICompiledCode } from './ICompiledCode'; -import { ISharedFunctionCollection } from '../Function/ISharedFunctionCollection'; - -export interface IFunctionCallCompiler { - compileCall( - call: ScriptFunctionCallData, - functions: ISharedFunctionCollection): ICompiledCode; -} diff --git a/src/application/Parser/Script/Compiler/ScriptCompiler.ts b/src/application/Parser/Script/Compiler/ScriptCompiler.ts index 75742901..185823a8 100644 --- a/src/application/Parser/Script/Compiler/ScriptCompiler.ts +++ b/src/application/Parser/Script/Compiler/ScriptCompiler.ts @@ -1,24 +1,25 @@ import { IScriptCode } from '@/domain/IScriptCode'; import { ScriptCode } from '@/domain/ScriptCode'; +import { ILanguageSyntax } from '@/domain/ScriptCode'; import { FunctionData, ScriptData } from 'js-yaml-loader!@/*'; import { IScriptCompiler } from './IScriptCompiler'; -import { ILanguageSyntax } from '@/domain/ScriptCode'; import { ISharedFunctionCollection } from './Function/ISharedFunctionCollection'; -import { IFunctionCallCompiler } from './FunctionCall/IFunctionCallCompiler'; -import { FunctionCallCompiler } from './FunctionCall/FunctionCallCompiler'; -import { IFunctionCompiler } from './Function/IFunctionCompiler'; -import { FunctionCompiler } from './Function/FunctionCompiler'; +import { IFunctionCallCompiler } from './Function/Call/Compiler/IFunctionCallCompiler'; +import { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler'; +import { ISharedFunctionsParser } from './Function/ISharedFunctionsParser'; +import { SharedFunctionsParser } from './Function/SharedFunctionsParser'; +import { parseFunctionCalls } from './Function/Call/FunctionCallParser'; export class ScriptCompiler implements IScriptCompiler { private readonly functions: ISharedFunctionCollection; constructor( functions: readonly FunctionData[] | undefined, private readonly syntax: ILanguageSyntax, - functionCompiler: IFunctionCompiler = FunctionCompiler.instance, + sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance, private readonly callCompiler: IFunctionCallCompiler = FunctionCallCompiler.instance, ) { if (!syntax) { throw new Error('undefined syntax'); } - this.functions = functionCompiler.compileFunctions(functions); + this.functions = sharedFunctionsParser.parseFunctions(functions); } public canCompile(script: ScriptData): boolean { if (!script) { throw new Error('undefined script'); } @@ -30,7 +31,8 @@ export class ScriptCompiler implements IScriptCompiler { public compile(script: ScriptData): IScriptCode { if (!script) { throw new Error('undefined script'); } try { - const compiledCode = this.callCompiler.compileCall(script.call, this.functions); + const calls = parseFunctionCalls(script.call); + const compiledCode = this.callCompiler.compileCall(calls, this.functions); return new ScriptCode( compiledCode.code, compiledCode.revertCode, diff --git a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts index 396a9c30..b10f6ca6 100644 --- a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts +++ b/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts @@ -4,8 +4,8 @@ import { CompositeExpressionParser } from '@/application/Parser/Script/Compiler/ import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { IProjectInformation } from '@/domain/IProjectInformation'; import { ICodeSubstituter } from './ICodeSubstituter'; -import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection'; -import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument'; +import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; export class CodeSubstituter implements ICodeSubstituter { constructor( diff --git a/src/application/collections/collection.yaml.d.ts b/src/application/collections/collection.yaml.d.ts index f505173b..505a2239 100644 --- a/src/application/collections/collection.yaml.d.ts +++ b/src/application/collections/collection.yaml.d.ts @@ -24,7 +24,7 @@ declare module 'js-yaml-loader!@/*' { readonly code?: string; readonly revertCode?: string; - readonly call?: ScriptFunctionCallData; + readonly call?: FunctionCallsData; } export interface ParameterDefinitionData { @@ -45,7 +45,7 @@ declare module 'js-yaml-loader!@/*' { readonly parameters?: FunctionCallParametersData; } - export type ScriptFunctionCallData = readonly FunctionCallData[] | FunctionCallData | undefined; + export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined; export interface ScriptData extends InstructionHolder, DocumentableData { readonly name: string; diff --git a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts index 5f03796f..d4f0a222 100644 --- a/tests/unit/application/Parser/CategoryCollectionParser.spec.ts +++ b/tests/unit/application/Parser/CategoryCollectionParser.spec.ts @@ -14,6 +14,7 @@ import { CategoryCollectionParseContextStub } from '@tests/unit/stubs/CategoryCo import { CategoryDataStub } from '@tests/unit/stubs/CategoryDataStub'; import { ScriptDataStub } from '@tests/unit/stubs/ScriptDataStub'; import { FunctionDataStub } from '@tests/unit/stubs/FunctionDataStub'; +import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub'; describe('CategoryCollectionParser', () => { describe('parseCategoryCollection', () => { @@ -105,9 +106,10 @@ describe('CategoryCollectionParser', () => { const expectedCode = 'code-from-the-function'; const functionName = 'function-name'; const scriptName = 'script-name'; - const script = ScriptDataStub.createWithCall({ function: functionName }) + const script = ScriptDataStub.createWithCall() + .withCall(new FunctionCallDataStub().withName(functionName).withParameters({})) .withName(scriptName); - const func = FunctionDataStub.createWithCode() + const func = FunctionDataStub.createWithCode().withParametersObject([]) .withName(functionName) .withCode(expectedCode); const category = new CategoryDataStub() diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/Expression.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/Expression.spec.ts index 85d112f8..fd882de2 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/Expression.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/Expression.spec.ts @@ -3,7 +3,7 @@ import { expect } from 'chai'; import { ExpressionPosition } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionPosition'; // tslint:disable-next-line:max-line-length import { ExpressionEvaluator, Expression } from '@/application/Parser/Script/Compiler/Expressions/Expression/Expression'; -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; import { FunctionParameterCollectionStub } from '@tests/unit/stubs/FunctionParameterCollectionStub'; import { FunctionCallArgumentStub } from '@tests/unit/stubs/FunctionCallArgumentStub'; diff --git a/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts b/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts index ea099ffa..38917dc9 100644 --- a/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext.spec.ts @@ -1,7 +1,7 @@ import 'mocha'; import { expect } from 'chai'; import { ExpressionEvaluationContext, IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler'; import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; import { PipelineCompilerStub } from '@tests/unit/stubs/PipelineCompilerStub'; diff --git a/tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts similarity index 92% rename from tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.spec.ts rename to tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts index 0f9c2df3..f45ebc43 100644 --- a/tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument.spec.ts @@ -1,7 +1,7 @@ import 'mocha'; import { expect } from 'chai'; -import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgument'; -import { testParameterName } from '../../ParameterNameTestRunner'; +import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { testParameterName } from '../../../ParameterNameTestRunner'; describe('FunctionCallArgument', () => { describe('ctor', () => { diff --git a/tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts similarity index 98% rename from tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection.spec.ts rename to tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts index 8e58698a..83e95aa0 100644 --- a/tests/unit/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection.spec.ts @@ -1,6 +1,6 @@ import 'mocha'; import { expect } from 'chai'; -import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/FunctionCallArgumentCollection'; +import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { FunctionCallArgumentStub } from '@tests/unit/stubs/FunctionCallArgumentStub'; describe('FunctionCallArgumentCollection', () => { diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts new file mode 100644 index 00000000..2fc46125 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler.spec.ts @@ -0,0 +1,505 @@ +import 'mocha'; +import { expect } from 'chai'; +import { FunctionCallParametersData } from 'js-yaml-loader!@/*'; +import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompiler'; +import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; +import { SharedFunctionCollectionStub } from '@tests/unit/stubs/SharedFunctionCollectionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { SharedFunctionStub } from '@tests/unit/stubs/SharedFunctionStub'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; +import { FunctionCallStub } from '@tests/unit/stubs/FunctionCallStub'; +import { ExpressionsCompilerStub } from '@tests/unit/stubs/ExpressionsCompilerStub'; + +describe('FunctionCallCompiler', () => { + describe('compileCall', () => { + describe('parameter validation', () => { + describe('call', () => { + it('throws with undefined call', () => { + // arrange + const expectedError = 'undefined calls'; + const call = undefined; + const functions = new SharedFunctionCollectionStub(); + const sut = new MockableFunctionCallCompiler(); + // act + const act = () => sut.compileCall(call, functions); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if call sequence has undefined call', () => { + // arrange + const expectedError = 'undefined function call'; + const call = [ + new FunctionCallStub(), + undefined, + ]; + const functions = new SharedFunctionCollectionStub(); + const sut = new MockableFunctionCallCompiler(); + // act + const act = () => sut.compileCall(call, functions); + // assert + expect(act).to.throw(expectedError); + }); + describe('throws if call parameters does not match function parameters', () => { + // arrange + const functionName = 'test-function-name'; + const testCases = [ + { + name: 'provided: single unexpected parameter, when: another expected', + functionParameters: [ 'expected-parameter' ], + callParameters: [ 'unexpected-parameter' ], + expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + `. Expected parameter(s): "expected-parameter"` + , + }, + { + name: 'provided: multiple unexpected parameters, when: different one is expected', + functionParameters: [ 'expected-parameter' ], + callParameters: [ 'unexpected-parameter1', 'unexpected-parameter2' ], + expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter1", "unexpected-parameter2"` + + `. Expected parameter(s): "expected-parameter"` + , + }, + { + name: 'provided: an unexpected parameter, when: multiple parameters are expected', + functionParameters: [ 'expected-parameter1', 'expected-parameter2' ], + callParameters: [ 'expected-parameter1', 'expected-parameter2', 'unexpected-parameter' ], + expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + `. Expected parameter(s): "expected-parameter1", "expected-parameter2"`, + }, + { + name: 'provided: an unexpected parameter, when: none required', + functionParameters: undefined, + callParameters: [ 'unexpected-call-parameter' ], + expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"` + + `. Expected parameter(s): none`, + }, + { + name: 'provided: expected and unexpected parameter, when: one of them is expected', + functionParameters: [ 'expected-parameter' ], + callParameters: [ 'expected-parameter', 'unexpected-parameter' ], + expectedError: `Function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"` + + `. Expected parameter(s): "expected-parameter"`, + }, + ]; + for (const testCase of testCases) { + it(testCase.name, () => { + const func = new SharedFunctionStub(FunctionBodyType.Code) + .withName('test-function-name') + .withParameterNames(...testCase.functionParameters); + let params: FunctionCallParametersData = {}; + for (const parameter of testCase.callParameters) { + params = {...params, [parameter]: 'defined-parameter-value '}; + } + const call = new FunctionCallStub() + .withFunctionName(func.name) + .withArguments(params); + const functions = new SharedFunctionCollectionStub() + .withFunction(func); + const sut = new MockableFunctionCallCompiler(); + // act + const act = () => sut.compileCall([call], functions); + // assert + expect(act).to.throw(testCase.expectedError); + }); + } + }); + }); + describe('functions', () => { + it('throws with undefined functions', () => { + // arrange + const expectedError = 'undefined functions'; + const call = new FunctionCallStub(); + const functions = undefined; + const sut = new MockableFunctionCallCompiler(); + // act + const act = () => sut.compileCall([call], functions); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if function does not exist', () => { + // arrange + const expectedError = 'function does not exist'; + const call = new FunctionCallStub(); + const functions: ISharedFunctionCollection = { + getFunctionByName: () => { throw new Error(expectedError); }, + }; + const sut = new MockableFunctionCallCompiler(); + // act + const act = () => sut.compileCall([call], functions); + // assert + expect(act).to.throw(expectedError); + }); + }); + + }); + describe('builds code as expected', () => { + describe('builds single call as expected', () => { + // arrange + const parametersTestCases = [ + { + name: 'empty parameters', + parameters: [], + callArgs: { }, + }, + { + name: 'non-empty parameters', + parameters: [ 'param1', 'param2' ], + callArgs: { param1: 'value1', param2: 'value2' }, + }, + ]; + for (const testCase of parametersTestCases) { + it(testCase.name, () => { + const expected = { + execute: 'expected code (execute)', + revert: 'expected code (revert)', + }; + const func = new SharedFunctionStub(FunctionBodyType.Code) + .withParameterNames(...testCase.parameters); + const functions = new SharedFunctionCollectionStub().withFunction(func); + const call = new FunctionCallStub() + .withFunctionName(func.name) + .withArguments(testCase.callArgs); + const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs); + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setup({ givenCode: func.body.code.do, givenArgs: args, result: expected.execute }) + .setup({ givenCode: func.body.code.revert, givenArgs: args, result: expected.revert }); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + // act + const actual = sut.compileCall([call], functions); + // assert + expect(actual.code).to.equal(expected.execute); + expect(actual.revertCode).to.equal(expected.revert); + }); + } + }); + it('builds call sequence as expected', () => { + // arrange + const firstFunction = new SharedFunctionStub(FunctionBodyType.Code) + .withName('first-function-name') + .withCode('first-function-code') + .withRevertCode('first-function-revert-code'); + const secondFunction = new SharedFunctionStub(FunctionBodyType.Code) + .withName('second-function-name') + .withParameterNames('testParameter') + .withCode('second-function-code') + .withRevertCode('second-function-revert-code'); + const secondCallArguments = { testParameter: 'testValue' }; + const calls = [ + new FunctionCallStub().withFunctionName(firstFunction.name).withArguments({}), + new FunctionCallStub().withFunctionName(secondFunction.name).withArguments(secondCallArguments), + ]; + const firstFunctionCallArgs = new FunctionCallArgumentCollectionStub(); + const secondFunctionCallArgs = new FunctionCallArgumentCollectionStub() + .withArguments(secondCallArguments); + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setupToReturnFunctionCode(firstFunction, firstFunctionCallArgs) + .setupToReturnFunctionCode(secondFunction, secondFunctionCallArgs); + const expectedExecute = `${firstFunction.body.code.do}\n${secondFunction.body.code.do}`; + const expectedRevert = `${firstFunction.body.code.revert}\n${secondFunction.body.code.revert}`; + const functions = new SharedFunctionCollectionStub() + .withFunction(firstFunction) + .withFunction(secondFunction); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + // act + const actual = sut.compileCall(calls, functions); + // assert + expect(actual.code).to.equal(expectedExecute); + expect(actual.revertCode).to.equal(expectedRevert); + }); + describe('can compile a call tree (function calling another)', () => { + describe('single deep function call', () => { + it('builds 2nd level of depth without arguments', () => { + // arrange + const emptyArgs = new FunctionCallArgumentCollectionStub(); + const deepFunctionName = 'deepFunction'; + const functions = { + deep: new SharedFunctionStub(FunctionBodyType.Code) + .withName(deepFunctionName) + .withCode('deep function code') + .withRevertCode('deep function final code'), + front: new SharedFunctionStub(FunctionBodyType.Calls) + .withName('frontFunction') + .withCalls(new FunctionCallStub() + .withFunctionName(deepFunctionName) + .withArgumentCollection(emptyArgs), + ), + }; + const expected = { + code: 'final code', + revert: 'final revert code', + }; + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setup({ + givenCode: functions.deep.body.code.do, + givenArgs: emptyArgs, + result: expected.code, + }) + .setup({ + givenCode: functions.deep.body.code.revert, + givenArgs: emptyArgs, + result: expected.revert, + }); + const mainCall = new FunctionCallStub() + .withFunctionName(functions.front.name) + .withArgumentCollection(emptyArgs); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + // act + const actual = sut.compileCall( + [mainCall], + new SharedFunctionCollectionStub().withFunction(functions.deep, functions.front), + ); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revert); + }); + it('builds 2nd level of depth by compiling arguments', () => { + // arrange + const scenario = { + front: { + functionName: 'frontFunction', + parameterName: 'frontFunctionParameterName', + args: { + fromMainCall: 'initial argument to be compiled', + toNextStatic: 'value from "front" to "deep" in function definition', + toNextCompiled: 'argument from "front" to "deep" (compiled)', + }, + callArgs: { + initialFromMainCall: () => new FunctionCallArgumentCollectionStub() + .withArgument(scenario.front.parameterName, scenario.front.args.fromMainCall), + expectedCallDeep: () => new FunctionCallArgumentCollectionStub() + .withArgument(scenario.deep.parameterName, scenario.front.args.toNextCompiled), + }, + getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) + .withName(scenario.front.functionName) + .withParameterNames(scenario.front.parameterName) + .withCalls(new FunctionCallStub() + .withFunctionName(scenario.deep.functionName) + .withArgument(scenario.deep.parameterName, scenario.front.args.toNextStatic), + ), + }, + deep: { + functionName: 'deepFunction', + parameterName: 'deepFunctionParameterName', + getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) + .withName(scenario.deep.functionName) + .withParameterNames(scenario.deep.parameterName) + .withCode(`${scenario.deep.functionName} function code`) + .withRevertCode(`${scenario.deep.functionName} function revert code`), + }, + }; + const expected = { + code: 'final code', + revert: 'final revert code', + }; + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setup({ // Front ===args===> Deep + givenCode: scenario.front.args.toNextStatic, + givenArgs: scenario.front.callArgs.initialFromMainCall(), + result: scenario.front.args.toNextCompiled, + }) + // set-up compiling of deep, compiled argument should be sent + .setup({ + givenCode: scenario.deep.getFunction().body.code.do, + givenArgs: scenario.front.callArgs.expectedCallDeep(), + result: expected.code, + }) + .setup({ + givenCode: scenario.deep.getFunction().body.code.revert, + givenArgs: scenario.front.callArgs.expectedCallDeep(), + result: expected.revert, + }); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + // act + const actual = sut.compileCall( + [ + new FunctionCallStub() + .withFunctionName(scenario.front.functionName) + .withArgumentCollection(scenario.front.callArgs.initialFromMainCall()), + ], + new SharedFunctionCollectionStub().withFunction( + scenario.deep.getFunction(), scenario.front.getFunction()), + ); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revert); + }); + it('builds 3rd level of depth by compiling arguments', () => { + // arrange + const scenario = { + first: { + functionName: 'firstFunction', + parameter: 'firstParameter', + args: { + fromMainCall: 'initial argument to be compiled', + toNextStatic: 'value from "first" to "second" in function definition', + toNextCompiled: 'argument from "first" to "second" (compiled)', + }, + callArgs: { + initialFromMainCall: () => new FunctionCallArgumentCollectionStub() + .withArgument(scenario.first.parameter, scenario.first.args.fromMainCall), + expectedToSecond: () => new FunctionCallArgumentCollectionStub() + .withArgument(scenario.second.parameter, scenario.first.args.toNextCompiled), + }, + getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) + .withName(scenario.first.functionName) + .withParameterNames(scenario.first.parameter) + .withCalls(new FunctionCallStub() + .withFunctionName(scenario.second.functionName) + .withArgument(scenario.second.parameter, scenario.first.args.toNextStatic), + ), + }, + second: { + functionName: 'secondFunction', + parameter: 'secondParameter', + args: { + toNextCompiled: 'argument second to third (compiled)', + toNextStatic: 'calling second to third', + }, + callArgs: { + expectedToThird: () => new FunctionCallArgumentCollectionStub() + .withArgument(scenario.third.parameter, scenario.second.args.toNextCompiled), + }, + getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) + .withName(scenario.second.functionName) + .withParameterNames(scenario.second.parameter) + .withCalls(new FunctionCallStub() + .withFunctionName(scenario.third.functionName) + .withArgument(scenario.third.parameter, scenario.second.args.toNextStatic), + ), + }, + third: { + functionName: 'thirdFunction', + parameter: 'thirdParameter', + getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) + .withName(scenario.third.functionName) + .withParameterNames(scenario.third.parameter) + .withCode(`${scenario.third.functionName} function code`) + .withRevertCode(`${scenario.third.functionName} function revert code`), + }, + }; + const expected = { + code: 'final code', + revert: 'final revert code', + }; + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setup({ // First ===args===> Second + givenCode: scenario.first.args.toNextStatic, + givenArgs: scenario.first.callArgs.initialFromMainCall(), + result: scenario.first.args.toNextCompiled, + }) + .setup({ // Second ===args===> third + givenCode: scenario.second.args.toNextStatic, + givenArgs: scenario.first.callArgs.expectedToSecond(), + result: scenario.second.args.toNextCompiled, + }) + // Compiling of third functions code with expected arguments + .setup({ + givenCode: scenario.third.getFunction().body.code.do, + givenArgs: scenario.second.callArgs.expectedToThird(), + result: expected.code, + }) + .setup({ + givenCode: scenario.third.getFunction().body.code.revert, + givenArgs: scenario.second.callArgs.expectedToThird(), + result: expected.revert, + }); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + const mainCall = new FunctionCallStub() + .withFunctionName(scenario.first.functionName) + .withArgumentCollection(scenario.first.callArgs.initialFromMainCall()); + // act + const actual = sut.compileCall( + [mainCall], + new SharedFunctionCollectionStub().withFunction( + scenario.first.getFunction(), + scenario.second.getFunction(), + scenario.third.getFunction()), + ); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revert); + }); + }); + describe('multiple deep function calls', () => { + it('builds 2nd level of depth without arguments', () => { + // arrange + const emptyArgs = new FunctionCallArgumentCollectionStub(); + const functions = { + call1: { + deep: { + functionName: 'deepFunction', + getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) + .withName(functions.call1.deep.functionName) + .withCode('deep function (1) code') + .withRevertCode('deep function (1) final code'), + }, + front: { + getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) + .withName('frontFunction') + .withCalls(new FunctionCallStub() + .withFunctionName(functions.call1.deep.functionName) + .withArgumentCollection(emptyArgs), + ), + }, + }, + call2: { + deep: { + functionName: 'deepFunction2', + getFunction: () => new SharedFunctionStub(FunctionBodyType.Code) + .withName(functions.call2.deep.functionName) + .withCode('deep function (2) code') + .withRevertCode('deep function (2) final code'), + }, + front: { + getFunction: () => new SharedFunctionStub(FunctionBodyType.Calls) + .withName('frontFunction2') + .withCalls(new FunctionCallStub() + .withFunctionName(functions.call2.deep.functionName) + .withArgumentCollection(emptyArgs), + ), + }, + }, + getMainCall: () => [ + new FunctionCallStub() + .withFunctionName(functions.call1.front.getFunction().name) + .withArgumentCollection(emptyArgs), + new FunctionCallStub() + .withFunctionName(functions.call2.front.getFunction().name) + .withArgumentCollection(emptyArgs), + ], + getCollection: () => new SharedFunctionCollectionStub().withFunction( + functions.call1.deep.getFunction(), + functions.call1.front.getFunction(), + functions.call2.deep.getFunction(), + functions.call2.front.getFunction(), + ), + }; + const expressionsCompilerMock = new ExpressionsCompilerStub() + .setupToReturnFunctionCode(functions.call1.deep.getFunction(), emptyArgs) + .setupToReturnFunctionCode(functions.call2.deep.getFunction(), emptyArgs); + const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); + const expected = { + code: `${functions.call1.deep.getFunction().body.code.do}\n${functions.call2.deep.getFunction().body.code.do}`, + revert: `${functions.call1.deep.getFunction().body.code.revert}\n${functions.call2.deep.getFunction().body.code.revert}`, + }; + // act + const actual = sut.compileCall( + functions.getMainCall(), + functions.getCollection(), + ); + // assert + expect(actual.code).to.equal(expected.code); + expect(actual.revertCode).to.equal(expected.revert); + }); + }); + }); + }); + }); +}); + +class MockableFunctionCallCompiler extends FunctionCallCompiler { + constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) { + super(expressionsCompiler); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts new file mode 100644 index 00000000..aaec848f --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCall.spec.ts @@ -0,0 +1,76 @@ +import 'mocha'; +import { expect } from 'chai'; +import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; + +describe('FunctionCall', () => { + describe('ctor', () => { + describe('args', () => { + it('throws when args is undefined', () => { + // arrange + const expectedError = 'undefined args'; + const args = undefined; + // act + const act = () => new FunctionCallBuilder() + .withArgs(args) + .build(); + // assert + expect(act).to.throw(expectedError); + }); + it('sets args as expected', () => { + // arrange + const expected = new FunctionCallArgumentCollectionStub() + .withArgument('testParameter', 'testValue'); + // act + const sut = new FunctionCallBuilder() + .withArgs(expected) + .build(); + // assert + expect(sut.args).to.deep.equal(expected); + }); + }); + describe('functionName', () => { + it('throws when function name is undefined', () => { + // arrange + const expectedError = 'empty function name in function call'; + const functionName = undefined; + // act + const act = () => new FunctionCallBuilder() + .withFunctionName(functionName) + .build(); + // assert + expect(act).to.throw(expectedError); + }); + it('sets function name as expected', () => { + // arrange + const expected = 'expectedFunctionName'; + // act + const sut = new FunctionCallBuilder() + .withFunctionName(expected) + .build(); + // assert + expect(sut.functionName).to.equal(expected); + }); + }); + }); +}); + +class FunctionCallBuilder { + private functionName = 'functionName'; + private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub(); + + public withFunctionName(functionName: string) { + this.functionName = functionName; + return this; + } + + public withArgs(args: IReadOnlyFunctionCallArgumentCollection) { + this.args = args; + return this; + } + + public build() { + return new FunctionCall(this.functionName, this.args); + } +} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.spec.ts new file mode 100644 index 00000000..6d1713a8 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/Call/FunctionCallParser.spec.ts @@ -0,0 +1,101 @@ +import 'mocha'; +import { expect } from 'chai'; +import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser'; +import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub'; + +describe('FunctionCallParser', () => { + describe('parseFunctionCalls', () => { + it('throws with undefined call', () => { + // arrange + const expectedError = 'undefined call data'; + const call = undefined; + // act + const act = () => parseFunctionCalls(call); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if call is not an object', () => { + // arrange + const expectedError = 'called function(s) must be an object'; + const invalidCalls: readonly any[] = ['string', 33]; + invalidCalls.forEach((invalidCall) => { + // act + const act = () => parseFunctionCalls(invalidCall); + // assert + expect(act).to.throw(expectedError); + }); + }); + it('throws if call sequence has undefined call', () => { + // arrange + const expectedError = 'undefined function call'; + const data = [ + new FunctionCallDataStub(), + undefined, + ]; + // act + const act = () => parseFunctionCalls(data); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if call sequence has undefined function name', () => { + // arrange + const expectedError = 'empty function name in function call'; + const data = [ + new FunctionCallDataStub().withName('function-name'), + new FunctionCallDataStub().withName(undefined), + ]; + // act + const act = () => parseFunctionCalls(data); + // assert + expect(act).to.throw(expectedError); + }); + it('parses single call as expected', () => { + // arrange + const expectedFunctionName = 'functionName'; + const expectedParameterName = 'parameterName'; + const expectedArgumentValue = 'argumentValue'; + const data = new FunctionCallDataStub() + .withName(expectedFunctionName) + .withParameters({ [expectedParameterName]: expectedArgumentValue }); + // act + const actual = parseFunctionCalls(data); + // assert + expect(actual).to.have.lengthOf(1); + const call = actual[0]; + expect(call.functionName).to.equal(expectedFunctionName); + const args = call.args; + expect(args.getAllParameterNames()).to.have.lengthOf(1); + expect(args.hasArgument(expectedParameterName)).to.equal(true, + `Does not include expected parameter: "${expectedParameterName}"\n` + + `But includes: "${args.getAllParameterNames()}"`); + const argument = args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + }); + it('parses multiple calls as expected', () => { + // arrange + const getFunctionName = (index: number) => `functionName${index}`; + const getParameterName = (index: number) => `parameterName${index}`; + const getArgumentValue = (index: number) => `argumentValue${index}`; + const createCall = (index: number) => new FunctionCallDataStub() + .withName(getFunctionName(index)) + .withParameters({ [getParameterName(index)]: getArgumentValue(index)}); + const calls = [ createCall(0), createCall(1), createCall(2), createCall(3) ]; + // act + const actual = parseFunctionCalls(calls); + // assert + expect(actual).to.have.lengthOf(calls.length); + for (let i = 0; i < calls.length; i++) { + const call = actual[i]; + const expectedParameterName = getParameterName(i); + const expectedArgumentValue = getArgumentValue(i); + expect(call.functionName).to.equal(getFunctionName(i)); + expect(call.args.getAllParameterNames()).to.have.lengthOf(1); + expect(call.args.hasArgument(expectedParameterName)).to.equal(true); + const argument = call.args.getArgument(expectedParameterName); + expect(argument.parameterName).to.equal(expectedParameterName); + expect(argument.argumentValue).to.equal(expectedArgumentValue); + } + }); + }); +}); diff --git a/tests/unit/application/Parser/Script/Compiler/Function/FunctionCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/FunctionCompiler.spec.ts deleted file mode 100644 index 05476861..00000000 --- a/tests/unit/application/Parser/Script/Compiler/Function/FunctionCompiler.spec.ts +++ /dev/null @@ -1,260 +0,0 @@ -import 'mocha'; -import { expect } from 'chai'; -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; -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', () => { - describe('validates functions', () => { - it('throws if one of the functions is undefined', () => { - // arrange - const expectedError = `some functions are undefined`; - const functions = [ FunctionDataStub.createWithCode(), undefined ]; - const sut = new MockableFunctionCompiler(); - // act - const act = () => sut.compileFunctions(functions); - // assert - expect(act).to.throw(expectedError); - }); - it('throws when functions have same names', () => { - // arrange - const name = 'same-func-name'; - const expectedError = `duplicate function name: "${name}"`; - const functions = [ - FunctionDataStub.createWithCode().withName(name), - FunctionDataStub.createWithCode().withName(name), - ]; - const sut = new MockableFunctionCompiler(); - // act - const act = () => sut.compileFunctions(functions); - // 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', () => { - // arrange - const code = 'duplicate-code'; - const expectedError = `duplicate "code" in functions: "${code}"`; - const functions = [ - FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code), - FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code), - ]; - const sut = new MockableFunctionCompiler(); - // act - const act = () => sut.compileFunctions(functions); - // assert - expect(act).to.throw(expectedError); - }); - it('revertCode', () => { - // arrange - const revertCode = 'duplicate-revert-code'; - const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`; - const functions = [ - FunctionDataStub.createWithoutCallOrCodes() - .withName('func-1').withCode('code-1').withRevertCode(revertCode), - FunctionDataStub.createWithoutCallOrCodes() - .withName('func-2').withCode('code-2').withRevertCode(revertCode), - ]; - const sut = new MockableFunctionCompiler(); - // act - const act = () => sut.compileFunctions(functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - it('both code and call are defined', () => { - // arrange - const functionName = 'invalid-function'; - const expectedError = `both "code" and "call" are defined in "${functionName}"`; - const invalidFunction = FunctionDataStub.createWithoutCallOrCodes() - .withName(functionName) - .withCode('code') - .withMockCall(); - const sut = new MockableFunctionCompiler(); - // act - const act = () => sut.compileFunctions([ invalidFunction ]); - // assert - expect(act).to.throw(expectedError); - }); - it('neither code and call is defined', () => { - // 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 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 - const emptyValues = [ [], undefined ]; - const sut = new MockableFunctionCompiler(); - for (const emptyFunctions of emptyValues) { - // act - const actual = sut.compileFunctions(emptyFunctions); - // assert - expect(actual).to.not.equal(undefined); - } - }); - it('parses single function with code as expected', () => { - // arrange - const name = 'function-name'; - const expected = FunctionDataStub - .createWithoutCallOrCodes() - .withName(name) - .withCode('expected-code') - .withRevertCode('expected-revert-code') - .withParameters( - new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true), - new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), - ); - const sut = new MockableFunctionCompiler(); - // act - const collection = sut.compileFunctions([ expected ]); - // expect - const actual = collection.getFunctionByName(name); - expectEqualFunctions(expected, actual); - }); - it('parses function with call as expected', () => { - // arrange - const calleeName = 'callee-function'; - const caller = FunctionDataStub.createWithoutCallOrCodes() - .withName('caller-function') - .withCall({ function: calleeName }); - const callee = FunctionDataStub.createWithoutCallOrCodes() - .withName(calleeName) - .withCode('expected-code') - .withRevertCode('expected-revert-code'); - const sut = new MockableFunctionCompiler(); - // act - const collection = sut.compileFunctions([ caller, callee ]); - // expect - const actual = collection.getFunctionByName(caller.name); - expectEqualFunctionCode(callee, actual); - }); - it('parses multiple functions with call as expected', () => { - // arrange - const calleeName = 'callee-function'; - const caller1 = FunctionDataStub.createWithoutCallOrCodes() - .withName('caller-function') - .withCall({ function: calleeName }); - const caller2 = FunctionDataStub.createWithoutCallOrCodes() - .withName('caller-function-2') - .withCall({ function: calleeName }); - const callee = FunctionDataStub.createWithoutCallOrCodes() - .withName(calleeName) - .withCode('expected-code') - .withRevertCode('expected-revert-code'); - const sut = new MockableFunctionCompiler(); - // act - const collection = sut.compileFunctions([ caller1, caller2, callee ]); - // expect - const compiledCaller1 = collection.getFunctionByName(caller1.name); - const compiledCaller2 = collection.getFunctionByName(caller2.name); - expectEqualFunctionCode(callee, compiledCaller1); - expectEqualFunctionCode(callee, compiledCaller2); - }); - }); -}); - -function expectEqualFunctions(expected: FunctionData, actual: ISharedFunction) { - expect(actual.name).to.equal(expected.name); - expect(areScrambledEqual(actual.parameters, expected.parameters)); - expectEqualFunctionCode(expected, actual); -} - -function expectEqualFunctionCode(expected: FunctionData, actual: ISharedFunction) { - expect(actual.code).to.equal(expected.code); - 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); - } -} diff --git a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts index 2af8baa8..4d324357 100644 --- a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunction.spec.ts @@ -1,116 +1,233 @@ 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'; +import { createCallerFunction, createFunctionWithInlineCode } from '@/application/Parser/Script/Compiler/Function/SharedFunction'; +import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCallStub } from '@tests/unit/stubs/FunctionCallStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; describe('SharedFunction', () => { describe('name', () => { - it('sets as expected', () => { - // arrange - const expected = 'expected-function-name'; - // act - const sut = new SharedFunctionBuilder() - .withName(expected) - .build(); - // assert - expect(sut.name).equal(expected); - }); - it('throws if empty or undefined', () => { - // arrange - const expectedError = 'undefined function name'; - const invalidValues = [ undefined, '' ]; - for (const invalidValue of invalidValues) { + runForEachFactoryMethod((build) => { + it('sets as expected', () => { + // arrange + const expected = 'expected-function-name'; + const builder = new SharedFunctionBuilder() + .withName(expected); // act - const act = () => new SharedFunctionBuilder() - .withName(invalidValue) - .build(); + const sut = build(builder); // assert - expect(act).to.throw(expectedError); - } + expect(sut.name).equal(expected); + }); + it('throws if empty or undefined', () => { + // arrange + const expectedError = 'undefined function name'; + const invalidValues = [ undefined, '' ]; + for (const invalidValue of invalidValues) { + const builder = new SharedFunctionBuilder() + .withName(invalidValue); + // act + const act = () => build(builder); + // assert + expect(act).to.throw(expectedError); + } + }); }); }); describe('parameters', () => { - it('sets as expected', () => { - // arrange - const expected = new FunctionParameterCollectionStub() - .withParameterName('test-parameter'); - // act - const sut = new SharedFunctionBuilder() - .withParameters(expected) - .build(); - // assert - expect(sut.parameters).to.equal(expected); - }); - it('throws if undefined', () => { - // arrange - const expectedError = 'undefined parameters'; - const parameters = undefined; - // act - const act = () => new SharedFunctionBuilder() - .withParameters(parameters) - .build(); - // assert - expect(act).to.throw(expectedError); - }); - }); - describe('code', () => { - it('sets as expected', () => { - // arrange - const expected = 'expected-code'; - // act - const sut = new SharedFunctionBuilder() - .withCode(expected) - .build(); - // assert - expect(sut.code).equal(expected); - }); - it('throws if empty or undefined', () => { - // arrange - const functionName = 'expected-function-name'; - const expectedError = `undefined function ("${functionName}") code`; - const invalidValues = [ undefined, '' ]; - for (const invalidValue of invalidValues) { + runForEachFactoryMethod((build) => { + it('sets as expected', () => { + // arrange + const expected = new FunctionParameterCollectionStub() + .withParameterName('test-parameter'); + const builder = new SharedFunctionBuilder() + .withParameters(expected); // act - const act = () => new SharedFunctionBuilder() - .withName(functionName) - .withCode(invalidValue) - .build(); + const sut = build(builder); + // assert + expect(sut.parameters).equal(expected); + }); + it('throws if undefined', () => { + // arrange + const expectedError = 'undefined parameters'; + const parameters = undefined; + const builder = new SharedFunctionBuilder() + .withParameters(parameters); + // act + const act = () => build(builder); // assert expect(act).to.throw(expectedError); - } + }); }); }); - describe('revertCode', () => { - it('sets as expected', () => { - // arrange - const testData = [ 'expected-revert-code', undefined, '' ]; - for (const data of testData) { + describe('body', () => { + describe('createFunctionWithInlineCode', () => { + describe('code', () => { + it('sets as expected', () => { + // arrange + const expected = 'expected-code'; + // act + const sut = new SharedFunctionBuilder() + .withCode(expected) + .createFunctionWithInlineCode(); + // assert + expect(sut.body.code.do).equal(expected); + }); + it('throws if empty or undefined', () => { + // arrange + const functionName = 'expected-function-name'; + const expectedError = `undefined code in function "${functionName}"`; + const invalidValues = [ undefined, '' ]; + for (const invalidValue of invalidValues) { + // act + const act = () => new SharedFunctionBuilder() + .withName(functionName) + .withCode(invalidValue) + .createFunctionWithInlineCode(); + // assert + expect(act).to.throw(expectedError); + } + }); + }); + describe('revertCode', () => { + it('sets as expected', () => { + // arrange + const testData = [ 'expected-revert-code', undefined, '' ]; + for (const data of testData) { + // act + const sut = new SharedFunctionBuilder() + .withRevertCode(data) + .createFunctionWithInlineCode(); + // assert + expect(sut.body.code.revert).equal(data); + } + }); + }); + it('sets type as expected', () => { + // arrange + const expectedType = FunctionBodyType.Code; // act const sut = new SharedFunctionBuilder() - .withRevertCode(data) - .build(); + .createFunctionWithInlineCode(); // assert - expect(sut.revertCode).equal(data); - } + expect(sut.body.type).equal(expectedType); + }); + it('calls are undefined', () => { + // arrange + const expectedCalls = undefined; + // act + const sut = new SharedFunctionBuilder() + .createFunctionWithInlineCode(); + // assert + expect(sut.body.calls).equal(expectedCalls); + }); + }); + describe('createCallerFunction', () => { + describe('callSequence', () => { + it('sets as expected', () => { + // arrange + const expected = [ + new FunctionCallStub().withFunctionName('firstFunction'), + new FunctionCallStub().withFunctionName('secondFunction'), + ]; + // act + const sut = new SharedFunctionBuilder() + .withCallSequence(expected) + .createCallerFunction(); + // assert + expect(sut.body.calls).equal(expected); + }); + it('throws if undefined', () => { + // arrange + const functionName = 'invalidFunction'; + const callSequence = undefined; + const expectedError = `undefined call sequence in function "${functionName}"`; + // act + const act = () => new SharedFunctionBuilder() + .withName(functionName) + .withCallSequence(callSequence) + .createCallerFunction(); + // assert + expect(act).to.throw(expectedError); + }); + it('throws if empty', () => { + // arrange + const functionName = 'invalidFunction'; + const callSequence = [ ]; + const expectedError = `empty call sequence in function "${functionName}"`; + // act + const act = () => new SharedFunctionBuilder() + .withName(functionName) + .withCallSequence(callSequence) + .createCallerFunction(); + // assert + expect(act).to.throw(expectedError); + }); + }); + it('sets type as expected', () => { + // arrange + const expectedType = FunctionBodyType.Calls; + // act + const sut = new SharedFunctionBuilder() + .createCallerFunction(); + // assert + expect(sut.body.type).equal(expectedType); + }); + it('code is undefined', () => { + // arrange + const expectedCode = undefined; + // act + const sut = new SharedFunctionBuilder() + .createCallerFunction(); + // assert + expect(sut.body.code).equal(expectedCode); + }); }); }); }); +function runForEachFactoryMethod( + act: (action: (sut: SharedFunctionBuilder) => ISharedFunction) => void): void { + describe('createCallerFunction', () => { + const action = (builder: SharedFunctionBuilder) => builder.createCallerFunction(); + act(action); + }); + describe('createFunctionWithInlineCode', () => { + const action = (builder: SharedFunctionBuilder) => builder.createFunctionWithInlineCode(); + act(action); + }); +} + +/* + Using an abstraction here allows for easy refactorings in + parameters or moving between functional and object-oriented + solutions without refactorings all tests. +*/ class SharedFunctionBuilder { private name = 'name'; private parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub(); + private callSequence: readonly IFunctionCall[] = [ new FunctionCallStub() ]; private code = 'code'; private revertCode = 'revert-code'; - public build(): SharedFunction { - return new SharedFunction( + public createCallerFunction(): ISharedFunction { + return createCallerFunction( + this.name, + this.parameters, + this.callSequence, + ); + } + public createFunctionWithInlineCode(): ISharedFunction { + return createFunctionWithInlineCode( this.name, this.parameters, this.code, this.revertCode, ); } + public withName(name: string) { this.name = name; return this; @@ -127,4 +244,8 @@ class SharedFunctionBuilder { this.revertCode = revertCode; return this; } + public withCallSequence(callSequence: readonly IFunctionCall[]) { + this.callSequence = callSequence; + return this; + } } diff --git a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionCollection.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionCollection.spec.ts index 410203b1..d3c8e00c 100644 --- a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionCollection.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionCollection.spec.ts @@ -2,6 +2,8 @@ import 'mocha'; import { expect } from 'chai'; import { SharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/SharedFunctionCollection'; import { SharedFunctionStub } from '@tests/unit/stubs/SharedFunctionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionCallStub } from '@tests/unit/stubs/FunctionCallStub'; describe('SharedFunctionCollection', () => { describe('addFunction', () => { @@ -19,7 +21,7 @@ describe('SharedFunctionCollection', () => { // arrange const functionName = 'duplicate-function'; const expectedError = `function with name ${functionName} already exists`; - const func = new SharedFunctionStub() + const func = new SharedFunctionStub(FunctionBodyType.Code) .withName('duplicate-function'); const sut = new SharedFunctionCollection(); sut.addFunction(func); @@ -27,7 +29,6 @@ describe('SharedFunctionCollection', () => { const act = () => sut.addFunction(func); // assert expect(act).to.throw(expectedError); - }); }); describe('getFunctionByName', () => { @@ -48,7 +49,7 @@ describe('SharedFunctionCollection', () => { // arrange const name = 'unique-name'; const expectedError = `called function is not defined "${name}"`; - const func = new SharedFunctionStub() + const func = new SharedFunctionStub(FunctionBodyType.Code) .withName('unexpected-name'); const sut = new SharedFunctionCollection(); sut.addFunction(func); @@ -57,18 +58,33 @@ describe('SharedFunctionCollection', () => { // assert expect(act).to.throw(expectedError); }); - it('returns existing function', () => { - // arrange - const name = 'expected-function-name'; - const expected = new SharedFunctionStub() - .withName(name); - const sut = new SharedFunctionCollection(); - sut.addFunction(new SharedFunctionStub().withName('another-function-name')); - sut.addFunction(expected); - // act - const actual = sut.getFunctionByName(name); - // assert - expect(actual).to.equal(expected); + describe('returns existing function', () => { + it('when function with inline code is added', () => { + // arrange + const expected = new SharedFunctionStub(FunctionBodyType.Code) + .withName('expected-function-name'); + const sut = new SharedFunctionCollection(); + // act + sut.addFunction(expected); + const actual = sut.getFunctionByName(expected.name); + // assert + expect(actual).to.equal(expected); + }); + it('when calling function is added', () => { + // arrange + const callee = new SharedFunctionStub(FunctionBodyType.Code) + .withName('calleeFunction'); + const caller = new SharedFunctionStub(FunctionBodyType.Calls) + .withName('callerFunction') + .withCalls(new FunctionCallStub().withFunctionName(callee.name)); + const sut = new SharedFunctionCollection(); + // act + sut.addFunction(callee); + sut.addFunction(caller); + const actual = sut.getFunctionByName(caller.name); + // assert + expect(actual).to.equal(caller); + }); }); }); }); diff --git a/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionsParser.spec.ts b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionsParser.spec.ts new file mode 100644 index 00000000..920e15f5 --- /dev/null +++ b/tests/unit/application/Parser/Script/Compiler/Function/SharedFunctionsParser.spec.ts @@ -0,0 +1,278 @@ +import 'mocha'; +import { expect } from 'chai'; +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionData } from 'js-yaml-loader!@/*'; +import { SharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/SharedFunctionsParser'; +import { FunctionDataStub } from '@tests/unit/stubs/FunctionDataStub'; +import { ParameterDefinitionDataStub } from '@tests/unit/stubs/ParameterDefinitionDataStub'; +import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter'; +import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub'; + +describe('SharedFunctionsParser', () => { + describe('parseFunctions', () => { + describe('validates functions', () => { + it('throws if one of the functions is undefined', () => { + // arrange + const expectedError = `some functions are undefined`; + const functions = [ FunctionDataStub.createWithCode(), undefined ]; + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions(functions); + // assert + expect(act).to.throw(expectedError); + }); + it('throws when functions have same names', () => { + // arrange + const name = 'same-func-name'; + const expectedError = `duplicate function name: "${name}"`; + const functions = [ + FunctionDataStub.createWithCode().withName(name), + FunctionDataStub.createWithCode().withName(name), + ]; + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions(functions); + // assert + expect(act).to.throw(expectedError); + }); + describe('throws when when function have duplicate code', () => { + it('code', () => { + // arrange + const code = 'duplicate-code'; + const expectedError = `duplicate "code" in functions: "${code}"`; + const functions = [ + FunctionDataStub.createWithoutCallOrCodes().withName('func-1').withCode(code), + FunctionDataStub.createWithoutCallOrCodes().withName('func-2').withCode(code), + ]; + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions(functions); + // assert + expect(act).to.throw(expectedError); + }); + it('revertCode', () => { + // arrange + const revertCode = 'duplicate-revert-code'; + const expectedError = `duplicate "revertCode" in functions: "${revertCode}"`; + const functions = [ + FunctionDataStub.createWithoutCallOrCodes() + .withName('func-1').withCode('code-1').withRevertCode(revertCode), + FunctionDataStub.createWithoutCallOrCodes() + .withName('func-2').withCode('code-2').withRevertCode(revertCode), + ]; + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions(functions); + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('ensures either call or code is defined', () => { + it('both code and call are defined', () => { + // arrange + const functionName = 'invalid-function'; + const expectedError = `both "code" and "call" are defined in "${functionName}"`; + const invalidFunction = FunctionDataStub.createWithoutCallOrCodes() + .withName(functionName) + .withCode('code') + .withMockCall(); + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions([ invalidFunction ]); + // assert + expect(act).to.throw(expectedError); + }); + it('neither code and call is defined', () => { + // arrange + const functionName = 'invalid-function'; + const expectedError = `neither "code" or "call" is defined in "${functionName}"`; + const invalidFunction = FunctionDataStub.createWithoutCallOrCodes() + .withName(functionName); + const sut = new SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions([ invalidFunction ]); + // 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 SharedFunctionsParser(); + // act + const act = () => sut.parseFunctions([ func ]); + // 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 SharedFunctionsParser(); + const act = () => sut.parseFunctions([ functionData ]); + + // assert + expect(act).to.throw(expectedError); + }); + }); + describe('empty functions', () => { + it('returns empty collection', () => { + // arrange + const emptyValues = [ [], undefined ]; + const sut = new SharedFunctionsParser(); + for (const emptyFunctions of emptyValues) { + // act + const actual = sut.parseFunctions(emptyFunctions); + // assert + expect(actual).to.not.equal(undefined); + } + }); + }); + describe('function with inline code', () => { + it('parses single function with code as expected', () => { + // arrange + const name = 'function-name'; + const expected = FunctionDataStub + .createWithoutCallOrCodes() + .withName(name) + .withCode('expected-code') + .withRevertCode('expected-revert-code') + .withParameters( + new ParameterDefinitionDataStub().withName('expectedParameter').withOptionality(true), + new ParameterDefinitionDataStub().withName('expectedParameter2').withOptionality(false), + ); + const sut = new SharedFunctionsParser(); + // act + const collection = sut.parseFunctions([ expected ]); + // expect + const actual = collection.getFunctionByName(name); + expectEqualName(expected, actual); + expectEqualParameters(expected, actual); + expectEqualFunctionWithInlineCode(expected, actual); + }); + }); + describe('function with calls', () => { + it('parses single function with call as expected', () => { + // arrange + const call = new FunctionCallDataStub() + .withName('calleeFunction') + .withParameters({test: 'value'}); + const data = FunctionDataStub.createWithoutCallOrCodes() + .withName('caller-function') + .withCall(call); + const sut = new SharedFunctionsParser(); + // act + const collection = sut.parseFunctions([ data ]); + // expect + const actual = collection.getFunctionByName(data.name); + expectEqualName(data, actual); + expectEqualParameters(data, actual); + expectEqualCalls([ call ], actual); + }); + it('parses multiple functions with call as expected', () => { + // arrange + const call1 = new FunctionCallDataStub() + .withName('calleeFunction1') + .withParameters({ param: 'value' }); + const call2 = new FunctionCallDataStub() + .withName('calleeFunction2') + .withParameters( {param2: 'value2'}); + const caller1 = FunctionDataStub.createWithoutCallOrCodes() + .withName('caller-function') + .withCall(call1); + const caller2 = FunctionDataStub.createWithoutCallOrCodes() + .withName('caller-function-2') + .withCall([ call1, call2 ]); + const sut = new SharedFunctionsParser(); + // act + const collection = sut.parseFunctions([ caller1, caller2 ]); + // expect + const compiledCaller1 = collection.getFunctionByName(caller1.name); + expectEqualName(caller1, compiledCaller1); + expectEqualParameters(caller1, compiledCaller1); + expectEqualCalls([ call1 ], compiledCaller1); + const compiledCaller2 = collection.getFunctionByName(caller2.name); + expectEqualName(caller2, compiledCaller2); + expectEqualParameters(caller2, compiledCaller2); + expectEqualCalls([ call1, call2 ], compiledCaller2); + }); + }); + }); +}); + +function expectEqualName( + expected: FunctionDataStub, actual: ISharedFunction): void { + expect(actual.name).to.equal(expected.name); +} + +function expectEqualParameters( + expected: FunctionDataStub, actual: ISharedFunction): void { + const actualSimplifiedParameters = actual.parameters.all.map((parameter) => ({ + name: parameter.name, + optional: parameter.isOptional, + })); + const expectedSimplifiedParameters = expected.parameters?.map((parameter) => ({ + name: parameter.name, + optional: parameter.optional || false, + })) || []; + expect(expectedSimplifiedParameters).to.deep.equal(actualSimplifiedParameters, 'Unequal parameters'); +} + +function expectEqualFunctionWithInlineCode( + expected: FunctionData, actual: ISharedFunction): void { + expect(actual.body, + `function "${actual.name}" has no body`); + expect(actual.body.code, + `function "${actual.name}" has no code`); + expect(actual.body.code.do).to.equal(expected.code); + expect(actual.body.code.revert).to.equal(expected.revertCode); +} + +function expectEqualCalls( + expected: FunctionCallDataStub[], actual: ISharedFunction) { + expect(actual.body, + `function "${actual.name}" has no body`); + expect(actual.body.calls, + `function "${actual.name}" has no calls`); + const actualSimplifiedCalls = actual.body.calls + .map((call) => ({ + function: call.functionName, + params: call.args.getAllParameterNames().map((name) => ({ + name, value: call.args.getArgument(name).argumentValue, + })), + })); + const expectedSimplifiedCalls = expected + .map((call) => ({ + function: call.function, + params: Object.keys(call.parameters).map((key) => ( + { name: key, value: call.parameters[key] } + )), + })); + expect(actualSimplifiedCalls).to.deep.equal(expectedSimplifiedCalls, 'Unequal calls'); +} diff --git a/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCall.spec.ts b/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCall.spec.ts deleted file mode 100644 index 3a614921..00000000 --- a/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCall.spec.ts +++ /dev/null @@ -1,51 +0,0 @@ -import 'mocha'; -import { expect } from 'chai'; -import { FunctionCall } from '@/application/Parser/Script/Compiler/FunctionCall/FunctionCall'; -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection'; -import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; - -describe('FunctionCall', () => { - describe('ctor', () => { - it('throws when args is undefined', () => { - // arrange - const expectedError = 'undefined args'; - const args = undefined; - // act - const act = () => new FunctionCallBuilder() - .withArgs(args) - .build(); - // assert - expect(act).to.throw(expectedError); - }); - it('throws when function name is undefined', () => { - // arrange - const expectedError = 'empty function name in function call'; - const functionName = undefined; - // act - const act = () => new FunctionCallBuilder() - .withFunctionName(functionName) - .build(); - // assert - expect(act).to.throw(expectedError); - }); - }); -}); - -class FunctionCallBuilder { - private functionName = 'functionName'; - private args: IReadOnlyFunctionCallArgumentCollection = new FunctionCallArgumentCollectionStub(); - - public withFunctionName(functionName: string) { - this.functionName = functionName; - return this; - } - - public withArgs(args: IReadOnlyFunctionCallArgumentCollection) { - this.args = args; - return this; - } - - public build() { - return new FunctionCall(this.functionName, this.args); - } -} diff --git a/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.spec.ts deleted file mode 100644 index b7641cce..00000000 --- a/tests/unit/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler.spec.ts +++ /dev/null @@ -1,219 +0,0 @@ -import 'mocha'; -import { expect } from 'chai'; -import { FunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*'; -import { FunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/FunctionCallCompiler'; -import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; -import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; -import { ExpressionsCompilerStub } from '@tests/unit/stubs/ExpressionsCompilerStub'; -import { SharedFunctionCollectionStub } from '@tests/unit/stubs/SharedFunctionCollectionStub'; -import { SharedFunctionStub } from '@tests/unit/stubs/SharedFunctionStub'; -import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; - -describe('FunctionCallCompiler', () => { - describe('compileCall', () => { - describe('parameter validation', () => { - describe('call', () => { - it('throws with undefined call', () => { - // arrange - const expectedError = 'undefined call'; - const call = undefined; - const functions = new SharedFunctionCollectionStub(); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - it('throws if call is not an object', () => { - // arrange - const expectedError = 'called function(s) must be an object'; - const invalidCalls: readonly any[] = ['string', 33]; - const sut = new MockableFunctionCallCompiler(); - const functions = new SharedFunctionCollectionStub(); - invalidCalls.forEach((invalidCall) => { - // act - const act = () => sut.compileCall(invalidCall, functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - it('throws if call sequence has undefined call', () => { - // arrange - const expectedError = 'undefined function call'; - const call: FunctionCallData[] = [ - { function: 'function-name' }, - undefined, - ]; - const functions = new SharedFunctionCollectionStub(); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - it('throws if call sequence has undefined function name', () => { - // arrange - const expectedError = 'empty function name in function call'; - const call: FunctionCallData[] = [ - { function: 'function-name' }, - { function: undefined }, - ]; - const functions = new SharedFunctionCollectionStub(); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - it('throws if call parameters does not match function parameters', () => { - // arrange - const functionName = 'test-function-name'; - const testCases = [ - { - name: 'an unexpected parameter instead', - functionParameters: [ 'another-parameter' ], - callParameters: [ 'unexpected-parameter' ], - expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`, - }, - { - name: 'an unexpected parameter when none required', - functionParameters: undefined, - callParameters: [ 'unexpected-parameter' ], - expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`, - }, - { - name: 'expected and unexpected parameter', - functionParameters: [ 'expected-parameter' ], - callParameters: [ 'expected-parameter', 'unexpected-parameter' ], - expectedError: `function "${functionName}" has unexpected parameter(s) provided: "unexpected-parameter"`, - }, - ]; - for (const testCase of testCases) { - it(testCase.name, () => { - const func = new SharedFunctionStub() - .withName('test-function-name') - .withParameterNames(...testCase.functionParameters); - let params: FunctionCallParametersData = {}; - for (const parameter of testCase.callParameters) { - params = {...params, [parameter]: 'defined-parameter-value '}; - } - const call: FunctionCallData = { function: func.name, parameters: params }; - const functions = new SharedFunctionCollectionStub() - .withFunction(func); - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(testCase.expectedError); - }); - } - }); - }); - describe('functions', () => { - it('throws with undefined functions', () => { - // arrange - const expectedError = 'undefined functions'; - const call: FunctionCallData = { function: 'function-call' }; - const functions = undefined; - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - it('throws if function does not exist', () => { - // arrange - const expectedError = 'function does not exist'; - const call: FunctionCallData = { function: 'function-call' }; - const functions: ISharedFunctionCollection = { - getFunctionByName: () => { throw new Error(expectedError); }, - }; - const sut = new MockableFunctionCallCompiler(); - // act - const act = () => sut.compileCall(call, functions); - // assert - expect(act).to.throw(expectedError); - }); - }); - - }); - describe('builds code as expected', () => { - describe('builds single call as expected', () => { - // arrange - const parametersTestCases = [ - { - name: 'empty parameters', - parameters: [], - callArgs: { }, - }, - { - name: 'non-empty parameters', - parameters: [ 'param1', 'param2' ], - callArgs: { param1: 'value1', param2: 'value2' }, - }, - ]; - for (const testCase of parametersTestCases) { - it(testCase.name, () => { - const expectedExecute = `expected-execute`; - const expectedRevert = `expected-revert`; - const func = new SharedFunctionStub().withParameterNames(...testCase.parameters); - const functions = new SharedFunctionCollectionStub().withFunction(func); - const call: FunctionCallData = { function: func.name, parameters: testCase.callArgs }; - const args = new FunctionCallArgumentCollectionStub().withArguments(testCase.callArgs); - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup(func.code, args, expectedExecute) - .setup(func.revertCode, args, expectedRevert); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall(call, functions); - // assert - expect(actual.code).to.equal(expectedExecute); - expect(actual.revertCode).to.equal(expectedRevert); - }); - } - }); - it('builds call sequence as expected', () => { - // arrange - const firstFunction = new SharedFunctionStub() - .withName('first-function-name') - .withCode('first-function-code') - .withRevertCode('first-function-revert-code'); - const secondFunction = new SharedFunctionStub() - .withName('second-function-name') - .withParameterNames('testParameter') - .withCode('second-function-code') - .withRevertCode('second-function-revert-code'); - const secondCallArguments = { testParameter: 'testValue' }; - const call: FunctionCallData[] = [ - { function: firstFunction.name }, - { function: secondFunction.name, parameters: secondCallArguments }, - ]; - const firstFunctionCallArgs = new FunctionCallArgumentCollectionStub(); - const secondFunctionCallArgs = new FunctionCallArgumentCollectionStub() - .withArguments(secondCallArguments); - const expressionsCompilerMock = new ExpressionsCompilerStub() - .setup(firstFunction.code, firstFunctionCallArgs, firstFunction.code) - .setup(firstFunction.revertCode, firstFunctionCallArgs, firstFunction.revertCode) - .setup(secondFunction.code, secondFunctionCallArgs, secondFunction.code) - .setup(secondFunction.revertCode, secondFunctionCallArgs, secondFunction.revertCode); - const expectedExecute = `${firstFunction.code}\n${secondFunction.code}`; - const expectedRevert = `${firstFunction.revertCode}\n${secondFunction.revertCode}`; - const functions = new SharedFunctionCollectionStub() - .withFunction(firstFunction) - .withFunction(secondFunction); - const sut = new MockableFunctionCallCompiler(expressionsCompilerMock); - // act - const actual = sut.compileCall(call, functions); - // assert - expect(actual.code).to.equal(expectedExecute); - expect(actual.revertCode).to.equal(expectedRevert); - }); - }); - }); -}); - -class MockableFunctionCallCompiler extends FunctionCallCompiler { - constructor(expressionsCompiler: IExpressionsCompiler = new ExpressionsCompilerStub()) { - super(expressionsCompiler); - } -} diff --git a/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts index 48f38cf0..b76ba571 100644 --- a/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts +++ b/tests/unit/application/Parser/Script/Compiler/ScriptCompiler.spec.ts @@ -1,17 +1,19 @@ import 'mocha'; import { expect } from 'chai'; -import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler'; import { FunctionData } from 'js-yaml-loader!@/*'; import { ILanguageSyntax } from '@/domain/ScriptCode'; -import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler'; -import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler'; -import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode'; +import { ScriptCompiler } from '@/application/Parser/Script/Compiler/ScriptCompiler'; +import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser'; +import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode'; +import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler'; import { LanguageSyntaxStub } from '@tests/unit/stubs/LanguageSyntaxStub'; import { ScriptDataStub } from '@tests/unit/stubs/ScriptDataStub'; import { FunctionDataStub } from '@tests/unit/stubs/FunctionDataStub'; import { FunctionCallCompilerStub } from '@tests/unit/stubs/FunctionCallCompilerStub'; -import { FunctionCompilerStub } from '@tests/unit/stubs/FunctionCompilerStub'; +import { SharedFunctionsParserStub } from '@tests/unit/stubs/SharedFunctionsParserStub'; import { SharedFunctionCollectionStub } from '@tests/unit/stubs/SharedFunctionCollectionStub'; +import { parseFunctionCalls } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCallParser'; +import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub'; describe('ScriptCompiler', () => { describe('ctor', () => { @@ -82,16 +84,17 @@ describe('ScriptCompiler', () => { code: 'expected-code', revertCode: 'expected-revert-code', }; - const script = ScriptDataStub.createWithCall(); + const call = new FunctionCallDataStub(); + const script = ScriptDataStub.createWithCall(call); const functions = [ FunctionDataStub.createWithCode().withName('existing-func') ]; const compiledFunctions = new SharedFunctionCollectionStub(); - const compilerMock = new FunctionCompilerStub(); - compilerMock.setup(functions, compiledFunctions); + const functionParserMock = new SharedFunctionsParserStub(); + functionParserMock.setup(functions, compiledFunctions); const callCompilerMock = new FunctionCallCompilerStub(); - callCompilerMock.setup(script.call, compiledFunctions, expected); + callCompilerMock.setup(parseFunctionCalls(call), compiledFunctions, expected); const sut = new ScriptCompilerBuilder() .withFunctions(...functions) - .withFunctionCompiler(compilerMock) + .withSharedFunctionsParser(functionParserMock) .withFunctionCallCompiler(callCompilerMock) .build(); // act @@ -171,7 +174,7 @@ class ScriptCompilerBuilder { } private functions: FunctionData[]; private syntax: ILanguageSyntax = new LanguageSyntaxStub(); - private functionCompiler: IFunctionCompiler = new FunctionCompilerStub(); + private sharedFunctionsParser: ISharedFunctionsParser = new SharedFunctionsParserStub(); private callCompiler: IFunctionCallCompiler = new FunctionCallCompilerStub(); public withFunctions(...functions: FunctionData[]): ScriptCompilerBuilder { this.functions = functions; @@ -193,8 +196,8 @@ class ScriptCompilerBuilder { this.syntax = syntax; return this; } - public withFunctionCompiler(functionCompiler: IFunctionCompiler): ScriptCompilerBuilder { - this.functionCompiler = functionCompiler; + public withSharedFunctionsParser(SharedFunctionsParser: ISharedFunctionsParser): ScriptCompilerBuilder { + this.sharedFunctionsParser = SharedFunctionsParser; return this; } public withFunctionCallCompiler(callCompiler: IFunctionCallCompiler): ScriptCompilerBuilder { @@ -205,6 +208,6 @@ class ScriptCompilerBuilder { if (!this.functions) { throw new Error('Function behavior not defined'); } - return new ScriptCompiler(this.functions, this.syntax, this.functionCompiler, this.callCompiler); + return new ScriptCompiler(this.functions, this.syntax, this.sharedFunctionsParser, this.callCompiler); } } diff --git a/tests/unit/stubs/ExpressionEvaluationContextStub.ts b/tests/unit/stubs/ExpressionEvaluationContextStub.ts index bacc512f..1d96a8cb 100644 --- a/tests/unit/stubs/ExpressionEvaluationContextStub.ts +++ b/tests/unit/stubs/ExpressionEvaluationContextStub.ts @@ -1,6 +1,6 @@ import { IExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { IPipelineCompiler } from '@/application/Parser/Script/Compiler/Expressions/Pipes/IPipelineCompiler'; -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub'; import { PipelineCompilerStub } from './PipelineCompilerStub'; diff --git a/tests/unit/stubs/ExpressionsCompilerStub.ts b/tests/unit/stubs/ExpressionsCompilerStub.ts index eb11b21c..09fa26e9 100644 --- a/tests/unit/stubs/ExpressionsCompilerStub.ts +++ b/tests/unit/stubs/ExpressionsCompilerStub.ts @@ -1,6 +1,8 @@ import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; -import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgumentCollection'; +import { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { scrambledEqual } from '@/application/Common/Array'; +import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { FunctionCallArgumentCollectionStub } from '@tests/unit/stubs/FunctionCallArgumentCollectionStub'; export class ExpressionsCompilerStub implements IExpressionsCompiler { @@ -8,32 +10,34 @@ export class ExpressionsCompilerStub implements IExpressionsCompiler { private readonly scenarios = new Array(); - public setup( - code: string, - parameters: IReadOnlyFunctionCallArgumentCollection, - result: string): ExpressionsCompilerStub { - this.scenarios.push({ code, parameters, result }); + public setup(scenario: ITestScenario): ExpressionsCompilerStub { + this.scenarios.push(scenario); return this; } + public setupToReturnFunctionCode(func: ISharedFunction, givenArgs: FunctionCallArgumentCollectionStub) { + return this + .setup({ givenCode: func.body.code.do, givenArgs, result: func.body.code.do }) + .setup({ givenCode: func.body.code.revert, givenArgs, result: func.body.code.revert }); + } 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)); + const scenario = this.scenarios.find((s) => s.givenCode === code && deepEqual(s.givenArgs, parameters)); if (scenario) { return scenario.result; } const parametersAndValues = parameters .getAllParameterNames() .map((name) => `${name}=${parameters.getArgument(name).argumentValue}`) - .join('", "'); - return `[ExpressionsCompilerStub] code: "${code}" | parameters: "${parametersAndValues}"`; + .join('\n\t'); + return `[ExpressionsCompilerStub]\ncode: "${code}"\nparameters: ${parametersAndValues}`; } } interface ITestScenario { - readonly code: string; - readonly parameters: IReadOnlyFunctionCallArgumentCollection; + readonly givenCode: string; + readonly givenArgs: IReadOnlyFunctionCallArgumentCollection; readonly result: string; } @@ -46,8 +50,8 @@ function deepEqual( return false; } for (const parameterName of expectedParameterNames) { - const expectedValue = expected.getArgument(parameterName); - const actualValue = expected.getArgument(parameterName); + const expectedValue = expected.getArgument(parameterName).argumentValue; + const actualValue = actual.getArgument(parameterName).argumentValue; if (expectedValue !== actualValue) { return false; } diff --git a/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts b/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts index 61ba5de6..eb806909 100644 --- a/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts +++ b/tests/unit/stubs/FunctionCallArgumentCollectionStub.ts @@ -1,6 +1,6 @@ // 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 { IFunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgument'; +import { IFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import { FunctionCallArgumentStub } from './FunctionCallArgumentStub'; export class FunctionCallArgumentCollectionStub implements IFunctionCallArgumentCollection { diff --git a/tests/unit/stubs/FunctionCallArgumentStub.ts b/tests/unit/stubs/FunctionCallArgumentStub.ts index e18d65a5..a663284b 100644 --- a/tests/unit/stubs/FunctionCallArgumentStub.ts +++ b/tests/unit/stubs/FunctionCallArgumentStub.ts @@ -1,5 +1,5 @@ // tslint:disable-next-line:max-line-length -import { IFunctionCallArgument } from '@/application/Parser/Script/Compiler/FunctionCall/Argument/IFunctionCallArgument'; +import { IFunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgument'; export class FunctionCallArgumentStub implements IFunctionCallArgument { public parameterName = 'stub-parameter-name'; diff --git a/tests/unit/stubs/FunctionCallCompilerStub.ts b/tests/unit/stubs/FunctionCallCompilerStub.ts index 6fe1916c..ee373223 100644 --- a/tests/unit/stubs/FunctionCallCompilerStub.ts +++ b/tests/unit/stubs/FunctionCallCompilerStub.ts @@ -1,26 +1,40 @@ +import { ICompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/ICompiledCode'; +import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/IFunctionCallCompiler'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; -import { ICompiledCode } from '@/application/Parser/Script/Compiler/FunctionCall/ICompiledCode'; -import { IFunctionCallCompiler } from '@/application/Parser/Script/Compiler/FunctionCall/IFunctionCallCompiler'; -import { FunctionCallData, ScriptFunctionCallData } from 'js-yaml-loader!@/*'; +import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; -interface Scenario { call: ScriptFunctionCallData; functions: ISharedFunctionCollection; result: ICompiledCode; } +interface IScenario { + calls: IFunctionCall[]; + functions: ISharedFunctionCollection; + result: ICompiledCode; +} export class FunctionCallCompilerStub implements IFunctionCallCompiler { - public scenarios = new Array(); - public setup(call: ScriptFunctionCallData, functions: ISharedFunctionCollection, result: ICompiledCode) { - this.scenarios.push({ call, functions, result }); + public scenarios = new Array(); + public setup( + calls: IFunctionCall[], + functions: ISharedFunctionCollection, + result: ICompiledCode) { + this.scenarios.push({ calls, functions, result }); } public compileCall( - call: ScriptFunctionCallData, + calls: IFunctionCall[], functions: ISharedFunctionCollection): ICompiledCode { - const predefined = this.scenarios.find((s) => s.call === call && s.functions === functions); + const predefined = this.scenarios.find((s) => areEqual(s.calls, calls) && s.functions === functions); if (predefined) { return predefined.result; } - const callee = functions.getFunctionByName((call as FunctionCallData).function); return { - code: callee.code, - revertCode: callee.revertCode, + code: 'function code [FunctionCallCompilerStub]', + revertCode: 'function revert code [FunctionCallCompilerStub]', }; } } + +function areEqual( + first: readonly IFunctionCall[], + second: readonly IFunctionCall[]) { + const comparer = (a: IFunctionCall, b: IFunctionCall) => a.functionName.localeCompare(b.functionName); + const printSorted = (calls: readonly IFunctionCall[]) => JSON.stringify([...calls].sort(comparer)); + return printSorted(first) === printSorted(second); +} diff --git a/tests/unit/stubs/FunctionCallDataStub.ts b/tests/unit/stubs/FunctionCallDataStub.ts new file mode 100644 index 00000000..98191de1 --- /dev/null +++ b/tests/unit/stubs/FunctionCallDataStub.ts @@ -0,0 +1,15 @@ +import { FunctionCallData, FunctionCallParametersData } from 'js-yaml-loader!@/*'; + +export class FunctionCallDataStub implements FunctionCallData { + public function = 'callDatStubCalleeFunction'; + public parameters: { [index: string]: string } = { testParameter : 'testArgument' }; + + public withName(functionName: string) { + this.function = functionName; + return this; + } + public withParameters(parameters: FunctionCallParametersData) { + this.parameters = parameters; + return this; + } +} diff --git a/tests/unit/stubs/FunctionCallStub.ts b/tests/unit/stubs/FunctionCallStub.ts new file mode 100644 index 00000000..7e7464ac --- /dev/null +++ b/tests/unit/stubs/FunctionCallStub.ts @@ -0,0 +1,24 @@ +import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; +import { FunctionCallArgumentCollectionStub } from './FunctionCallArgumentCollectionStub'; + +export class FunctionCallStub implements IFunctionCall { + public functionName = 'functionCallStub'; + public args = new FunctionCallArgumentCollectionStub(); + + public withFunctionName(functionName: string) { + this.functionName = functionName; + return this; + } + public withArgument(parameterName: string, argumentValue: string) { + this.args.withArgument(parameterName, argumentValue); + return this; + } + public withArguments(args: { readonly [index: string]: string }) { + this.args.withArguments(args); + return this; + } + public withArgumentCollection(args: FunctionCallArgumentCollectionStub) { + this.args = args; + return this; + } +} diff --git a/tests/unit/stubs/FunctionCodeStub.ts b/tests/unit/stubs/FunctionCodeStub.ts new file mode 100644 index 00000000..2cdc9019 --- /dev/null +++ b/tests/unit/stubs/FunctionCodeStub.ts @@ -0,0 +1,14 @@ +import { IFunctionCode } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; + +export class FunctionCodeStub implements IFunctionCode { + public do: string = 'do code (function-code-stub)'; + public revert?: string = 'revert code (function-code-stub)'; + public withDo(code: string) { + this.do = code; + return this; + } + public withRevert(revert: string) { + this.revert = revert; + return this; + } +} diff --git a/tests/unit/stubs/FunctionDataStub.ts b/tests/unit/stubs/FunctionDataStub.ts index f589127f..205a5f53 100644 --- a/tests/unit/stubs/FunctionDataStub.ts +++ b/tests/unit/stubs/FunctionDataStub.ts @@ -1,4 +1,5 @@ -import { FunctionData, ParameterDefinitionData, ScriptFunctionCallData } from 'js-yaml-loader!@/*'; +import { FunctionData, ParameterDefinitionData, FunctionCallsData } from 'js-yaml-loader!@/*'; +import { FunctionCallDataStub } from './FunctionCallDataStub'; export class FunctionDataStub implements FunctionData { public static createWithCode() { @@ -6,7 +7,7 @@ export class FunctionDataStub implements FunctionData { .withCode('stub-code') .withRevertCode('stub-revert-code'); } - public static createWithCall(call?: ScriptFunctionCallData) { + public static createWithCall(call?: FunctionCallsData) { let instance = new FunctionDataStub(); if (call) { instance = instance.withCall(call); @@ -22,10 +23,10 @@ export class FunctionDataStub implements FunctionData { public name = 'functionDataStub'; public code: string; public revertCode: string; - public call?: ScriptFunctionCallData; + public call?: FunctionCallsData; public parameters?: readonly ParameterDefinitionData[]; - private constructor() { } + private constructor() { /* use static factory methods to create an instance */ } public withName(name: string) { this.name = name; @@ -46,12 +47,12 @@ export class FunctionDataStub implements FunctionData { this.revertCode = revertCode; return this; } - public withCall(call: ScriptFunctionCallData) { + public withCall(call: FunctionCallsData) { this.call = call; return this; } public withMockCall() { - this.call = { function: 'func' }; + this.call = new FunctionCallDataStub(); return this; } } diff --git a/tests/unit/stubs/ScriptDataStub.ts b/tests/unit/stubs/ScriptDataStub.ts index 06cb4596..0a52b62e 100644 --- a/tests/unit/stubs/ScriptDataStub.ts +++ b/tests/unit/stubs/ScriptDataStub.ts @@ -1,5 +1,6 @@ import { RecommendationLevel } from '@/domain/RecommendationLevel'; -import { ScriptFunctionCallData, ScriptData } from 'js-yaml-loader!@/*'; +import { FunctionCallData, ScriptData } from 'js-yaml-loader!@/*'; +import { FunctionCallDataStub } from '@tests/unit/stubs/FunctionCallDataStub'; export class ScriptDataStub implements ScriptData { public static createWithCode(): ScriptDataStub { @@ -7,7 +8,7 @@ export class ScriptDataStub implements ScriptData { .withCode('stub-code') .withRevertCode('stub-revert-code'); } - public static createWithCall(call?: ScriptFunctionCallData): ScriptDataStub { + public static createWithCall(call?: FunctionCallData): ScriptDataStub { let instance = new ScriptDataStub(); if (call) { instance = instance.withCall(call); @@ -27,7 +28,7 @@ export class ScriptDataStub implements ScriptData { public recommend = RecommendationLevel[RecommendationLevel.Standard].toLowerCase(); public docs = ['hello.com']; - private constructor() { } + private constructor() { /* use static methods for constructing */ } public withName(name: string): ScriptDataStub { this.name = name; @@ -46,10 +47,10 @@ export class ScriptDataStub implements ScriptData { return this; } public withMockCall(): ScriptDataStub { - this.call = { function: 'func', parameters: [] }; + this.call = new FunctionCallDataStub(); return this; } - public withCall(call: ScriptFunctionCallData): ScriptDataStub { + public withCall(call: FunctionCallData): ScriptDataStub { this.call = call; return this; } diff --git a/tests/unit/stubs/SharedFunctionCollectionStub.ts b/tests/unit/stubs/SharedFunctionCollectionStub.ts index e4997d5f..c68ea9ac 100644 --- a/tests/unit/stubs/SharedFunctionCollectionStub.ts +++ b/tests/unit/stubs/SharedFunctionCollectionStub.ts @@ -1,18 +1,21 @@ import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import { SharedFunctionStub } from './SharedFunctionStub'; +import { FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; export class SharedFunctionCollectionStub implements ISharedFunctionCollection { private readonly functions = new Map(); - public withFunction(func: ISharedFunction) { - this.functions.set(func.name, func); + public withFunction(...funcs: readonly ISharedFunction[]) { + for (const func of funcs) { + this.functions.set(func.name, func); + } return this; } public getFunctionByName(name: string): ISharedFunction { if (this.functions.has(name)) { return this.functions.get(name); } - return new SharedFunctionStub() + return new SharedFunctionStub(FunctionBodyType.Code) .withName(name) .withCode('code by SharedFunctionCollectionStub') .withRevertCode('revert-code by SharedFunctionCollectionStub'); diff --git a/tests/unit/stubs/SharedFunctionStub.ts b/tests/unit/stubs/SharedFunctionStub.ts index b08c5fa7..4f5a28bd 100644 --- a/tests/unit/stubs/SharedFunctionStub.ts +++ b/tests/unit/stubs/SharedFunctionStub.ts @@ -1,13 +1,33 @@ -import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; +import { ISharedFunction, ISharedFunctionBody, FunctionBodyType } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { IReadOnlyFunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/IFunctionParameterCollection'; import { FunctionParameterCollectionStub } from './FunctionParameterCollectionStub'; +import { FunctionCallStub } from './FunctionCallStub'; +import { IFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/IFunctionCall'; export class SharedFunctionStub implements ISharedFunction { public name = 'shared-function-stub-name'; public parameters: IReadOnlyFunctionParameterCollection = new FunctionParameterCollectionStub() .withParameterName('shared-function-stub-parameter-name'); - public code = 'shared-function-stub-code'; - public revertCode = 'shared-function-stub-revert-code'; + + private code = 'shared-function-stub-code'; + private revertCode = 'shared-function-stub-revert-code'; + private bodyType: FunctionBodyType = FunctionBodyType.Code; + private calls: IFunctionCall[] = [ new FunctionCallStub() ]; + + constructor(type: FunctionBodyType) { + this.bodyType = type; + } + + public get body(): ISharedFunctionBody { + return { + type: this.bodyType, + code: this.bodyType === FunctionBodyType.Code ? { + do: this.code, + revert: this.revertCode, + } : undefined, + calls: this.bodyType === FunctionBodyType.Calls ? this.calls : undefined, + }; + } public withName(name: string) { this.name = name; @@ -25,6 +45,10 @@ export class SharedFunctionStub implements ISharedFunction { this.parameters = parameters; return this; } + public withCalls(...calls: readonly IFunctionCall[]) { + this.calls = [...calls]; + return this; + } public withParameterNames(...parameterNames: readonly string[]) { let collection = new FunctionParameterCollectionStub(); for (const name of parameterNames) { diff --git a/tests/unit/stubs/FunctionCompilerStub.ts b/tests/unit/stubs/SharedFunctionsParserStub.ts similarity index 78% rename from tests/unit/stubs/FunctionCompilerStub.ts rename to tests/unit/stubs/SharedFunctionsParserStub.ts index fea7a94c..73f6e88d 100644 --- a/tests/unit/stubs/FunctionCompilerStub.ts +++ b/tests/unit/stubs/SharedFunctionsParserStub.ts @@ -1,10 +1,10 @@ import { sequenceEqual } from '@/application/Common/Array'; -import { IFunctionCompiler } from '@/application/Parser/Script/Compiler/Function/IFunctionCompiler'; import { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; +import { ISharedFunctionsParser } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionsParser'; import { FunctionData } from 'js-yaml-loader!@/*'; import { SharedFunctionCollectionStub } from './SharedFunctionCollectionStub'; -export class FunctionCompilerStub implements IFunctionCompiler { +export class SharedFunctionsParserStub implements ISharedFunctionsParser { private setupResults = new Array<{ functions: readonly FunctionData[], result: ISharedFunctionCollection, @@ -14,7 +14,7 @@ export class FunctionCompilerStub implements IFunctionCompiler { this.setupResults.push( { functions, result }); } - public compileFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection { + public parseFunctions(functions: readonly FunctionData[]): ISharedFunctionCollection { const result = this.findResult(functions); return result || new SharedFunctionCollectionStub(); }