Refactor to improve iterations
- Use function abstractions (such as map, reduce, filter etc.) over for-of loops to gain benefits of having less side effects and easier readability. - Enable `downLevelIterations` for writing modern code with lazy evaluation. - Refactor for of loops to named abstractions to clearly express their intentions without needing to analyse the loop itself. - Add missing cases for changes that had no tests.
This commit is contained in:
@@ -42,13 +42,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = script.split(/\r\n|\r|\n/).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
for (const position of positions) {
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (position.endLine > totalLines) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`script end line (${position.endLine}) is out of range.`
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
+ `(total code lines: ${totalLines}`,
|
+ `(total code lines: ${totalLines}).`,
|
||||||
);
|
);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,9 +17,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
for (const line of lines) {
|
this.lines.push(...lines);
|
||||||
this.lines.push(line);
|
|
||||||
}
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,15 +19,14 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
let builder = this.codeBuilderFactory.create(scriptingDefinition.language);
|
||||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||||
for (const selection of selectedScripts) {
|
const scriptPositions = selectedScripts.reduce((result, selection) => {
|
||||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
return appendSelection(selection, result, builder);
|
||||||
}
|
}, new Map<SelectedScript, ICodePosition>());
|
||||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||||
return { code, scriptPositions };
|
return { code, scriptPositions };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,10 +17,8 @@ export class UserSelection implements IUserSelection {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
) {
|
) {
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
if (selectedScripts && selectedScripts.length > 0) {
|
for (const script of selectedScripts) {
|
||||||
for (const script of selectedScripts) {
|
this.scripts.addItem(script);
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -58,18 +56,19 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
||||||
const category = this.collection.findCategory(categoryId);
|
const scriptsToAddOrUpdate = this.collection
|
||||||
const scriptsToAddOrUpdate = category.getAllScriptsRecursively()
|
.findCategory(categoryId)
|
||||||
|
.getAllScriptsRecursively()
|
||||||
.filter(
|
.filter(
|
||||||
(script) => !this.scripts.exists(script.id)
|
(script) => !this.scripts.exists(script.id)
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|| this.scripts.getById(script.id).revert !== revert,
|
||||||
);
|
)
|
||||||
|
.map((script) => new SelectedScript(script, revert));
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
if (!scriptsToAddOrUpdate.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
for (const script of scriptsToAddOrUpdate) {
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
this.scripts.addOrUpdateItem(script);
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -106,11 +105,12 @@ export class UserSelection implements IUserSelection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
for (const script of this.collection.getAllScripts()) {
|
const scriptsToSelect = this.collection
|
||||||
if (!this.scripts.exists(script.id)) {
|
.getAllScripts()
|
||||||
const selection = new SelectedScript(script, false);
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
this.scripts.addItem(selection);
|
.map((script) => new SelectedScript(script, false));
|
||||||
}
|
for (const script of scriptsToSelect) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
@@ -135,10 +135,11 @@ export class UserSelection implements IUserSelection {
|
|||||||
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
.forEach((scriptId) => this.scripts.removeItem(scriptId));
|
||||||
}
|
}
|
||||||
// Select from unselected scripts
|
// Select from unselected scripts
|
||||||
const unselectedScripts = scripts.filter((script) => !this.scripts.exists(script.id));
|
const unselectedScripts = scripts
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new SelectedScript(script, false));
|
||||||
for (const toSelect of unselectedScripts) {
|
for (const toSelect of unselectedScripts) {
|
||||||
const selection = new SelectedScript(toSelect, false);
|
this.scripts.addItem(toSelect);
|
||||||
this.scripts.addItem(selection);
|
|
||||||
}
|
}
|
||||||
this.changed.notify(this.scripts.getItems());
|
this.changed.notify(this.scripts.getItems());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { CollectionData } from 'js-yaml-loader!@/*';
|
import { CollectionData } from 'js-yaml-loader!@/*';
|
||||||
import { Category } from '@/domain/Category';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
@@ -18,11 +17,7 @@ export function parseCategoryCollection(
|
|||||||
const scripting = new ScriptingDefinitionParser()
|
const scripting = new ScriptingDefinitionParser()
|
||||||
.parse(content.scripting, info);
|
.parse(content.scripting, info);
|
||||||
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
const categories = new Array<Category>();
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
for (const action of content.actions) {
|
|
||||||
const category = parseCategory(action, context);
|
|
||||||
categories.push(category);
|
|
||||||
}
|
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
const collection = new CategoryCollection(
|
const collection = new CategoryCollection(
|
||||||
os,
|
os,
|
||||||
|
|||||||
@@ -9,11 +9,6 @@ import { parseScript } from './Script/ScriptParser';
|
|||||||
|
|
||||||
let categoryIdCounter = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
interface ICategoryChildren {
|
|
||||||
subCategories: Category[];
|
|
||||||
subScripts: Script[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function parseCategory(
|
export function parseCategory(
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
@@ -48,6 +43,11 @@ function ensureValid(category: CategoryData) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ICategoryChildren {
|
||||||
|
subCategories: Category[];
|
||||||
|
subScripts: Script[];
|
||||||
|
}
|
||||||
|
|
||||||
function parseCategoryChild(
|
function parseCategoryChild(
|
||||||
data: CategoryOrScriptData,
|
data: CategoryOrScriptData,
|
||||||
children: ICategoryChildren,
|
children: ICategoryChildren,
|
||||||
|
|||||||
@@ -56,14 +56,13 @@ function compileExpressions(
|
|||||||
function extractRequiredParameterNames(
|
function extractRequiredParameterNames(
|
||||||
expressions: readonly IExpression[],
|
expressions: readonly IExpression[],
|
||||||
): string[] {
|
): string[] {
|
||||||
const usedParameterNames = expressions
|
return expressions
|
||||||
.map((e) => e.parameters.all
|
.map((e) => e.parameters.all
|
||||||
.filter((p) => !p.isOptional)
|
.filter((p) => !p.isOptional)
|
||||||
.map((p) => p.name))
|
.map((p) => p.name))
|
||||||
.filter((p) => p)
|
.filter(Boolean) // Remove empty or undefined
|
||||||
.flat();
|
.flat()
|
||||||
const uniqueParameterNames = Array.from(new Set(usedParameterNames));
|
.filter((name, index, array) => array.indexOf(name) === index); // Remove duplicates
|
||||||
return uniqueParameterNames;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureParamsUsedInCodeHasArgsProvided(
|
function ensureParamsUsedInCodeHasArgsProvided(
|
||||||
|
|||||||
@@ -16,13 +16,8 @@ export class CompositeExpressionParser implements IExpressionParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
const expressions = new Array<IExpression>();
|
return this.leafs.flatMap(
|
||||||
for (const parser of this.leafs) {
|
(parser) => parser.findExpressions(code) || [],
|
||||||
const newExpressions = parser.findExpressions(code);
|
);
|
||||||
if (newExpressions && newExpressions.length) {
|
|
||||||
expressions.push(...newExpressions);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return expressions;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,35 +18,42 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
if (!code) {
|
if (!code) {
|
||||||
throw new Error('undefined code');
|
throw new Error('undefined code');
|
||||||
}
|
}
|
||||||
const matches = Array.from(code.matchAll(this.regex));
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
let position: ExpressionPosition;
|
|
||||||
try {
|
|
||||||
position = new ExpressionPosition(startPos, endPos);
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`[${this.constructor.name}] invalid script position: ${error.message}\nRegex ${this.regex}\nCode: ${code}`);
|
|
||||||
}
|
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const parameters = getParameters(primitiveExpression);
|
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
||||||
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
yield expression;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private doOrRethrow<T>(action: () => T, errorText: string, code: string): T {
|
||||||
|
try {
|
||||||
|
return action();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`[${this.constructor.name}] ${errorText}: ${error.message}\nRegex: ${this.regex}\nCode: ${code}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
const endPos = startPos + match[0].length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createParameters(
|
||||||
|
expression: IPrimitiveExpression,
|
||||||
|
): FunctionParameterCollection {
|
||||||
|
return (expression.parameters || [])
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
|
parameters.addParameter(parameter);
|
||||||
|
return parameters;
|
||||||
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IPrimitiveExpression {
|
export interface IPrimitiveExpression {
|
||||||
evaluator: ExpressionEvaluator;
|
evaluator: ExpressionEvaluator;
|
||||||
parameters?: readonly IFunctionParameter[];
|
parameters?: readonly IFunctionParameter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function getParameters(
|
|
||||||
expression: IPrimitiveExpression,
|
|
||||||
): FunctionParameterCollection {
|
|
||||||
const parameters = new FunctionParameterCollection();
|
|
||||||
for (const parameter of expression.parameters || []) {
|
|
||||||
parameters.addParameter(parameter);
|
|
||||||
}
|
|
||||||
return parameters;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,11 +8,9 @@ export class PipelineCompiler implements IPipelineCompiler {
|
|||||||
ensureValidArguments(value, pipeline);
|
ensureValidArguments(value, pipeline);
|
||||||
const pipeNames = extractPipeNames(pipeline);
|
const pipeNames = extractPipeNames(pipeline);
|
||||||
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
const pipes = pipeNames.map((pipeName) => this.factory.get(pipeName));
|
||||||
let valueInCompilation = value;
|
return pipes.reduce((previousValue, pipe) => {
|
||||||
for (const pipe of pipes) {
|
return pipe.apply(previousValue);
|
||||||
valueInCompilation = pipe.apply(valueInCompilation);
|
}, value);
|
||||||
}
|
|
||||||
return valueInCompilation;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,11 +47,8 @@ interface ICompiledFunctionCall {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
function compileCallSequence(context: ICompilationContext): ICompiledFunctionCall {
|
||||||
const compiledFunctions = new Array<ICompiledFunctionCall>();
|
const compiledFunctions = context.callSequence
|
||||||
for (const call of context.callSequence) {
|
.flatMap((call) => compileSingleCall(call, context));
|
||||||
const compiledCode = compileSingleCall(call, context);
|
|
||||||
compiledFunctions.push(...compiledCode);
|
|
||||||
}
|
|
||||||
return {
|
return {
|
||||||
code: merge(compiledFunctions.map((f) => f.code)),
|
code: merge(compiledFunctions.map((f) => f.code)),
|
||||||
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
revertCode: merge(compiledFunctions.map((f) => f.revertCode)),
|
||||||
@@ -94,14 +91,17 @@ function compileArgs(
|
|||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
compiler: IExpressionsCompiler,
|
compiler: IExpressionsCompiler,
|
||||||
): IReadOnlyFunctionCallArgumentCollection {
|
): IReadOnlyFunctionCallArgumentCollection {
|
||||||
const compiledArgs = new FunctionCallArgumentCollection();
|
return argsToCompile
|
||||||
for (const parameterName of argsToCompile.getAllParameterNames()) {
|
.getAllParameterNames()
|
||||||
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
.map((parameterName) => {
|
||||||
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
const { argumentValue } = argsToCompile.getArgument(parameterName);
|
||||||
const newArgument = new FunctionCallArgument(parameterName, compiledValue);
|
const compiledValue = compiler.compileExpressions(argumentValue, args);
|
||||||
compiledArgs.addArgument(newArgument);
|
return new FunctionCallArgument(parameterName, compiledValue);
|
||||||
}
|
})
|
||||||
return compiledArgs;
|
.reduce((compiledArgs, arg) => {
|
||||||
|
compiledArgs.addArgument(arg);
|
||||||
|
return compiledArgs;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function merge(codeParts: readonly string[]): string {
|
function merge(codeParts: readonly string[]): string {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionCallData, FunctionCallsData } from 'js-yaml-loader!@/*';
|
import { FunctionCallData, FunctionCallsData, FunctionCallParametersData } from 'js-yaml-loader!@/*';
|
||||||
import { IFunctionCall } from './IFunctionCall';
|
import { IFunctionCall } from './IFunctionCall';
|
||||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
||||||
@@ -26,10 +26,17 @@ function parseFunctionCall(call: FunctionCallData): IFunctionCall {
|
|||||||
if (!call) {
|
if (!call) {
|
||||||
throw new Error('undefined function call');
|
throw new Error('undefined function call');
|
||||||
}
|
}
|
||||||
const args = new FunctionCallArgumentCollection();
|
const callArgs = parseArgs(call.parameters);
|
||||||
for (const parameterName of Object.keys(call.parameters || {})) {
|
return new FunctionCall(call.function, callArgs);
|
||||||
const arg = new FunctionCallArgument(parameterName, call.parameters[parameterName]);
|
}
|
||||||
args.addArgument(arg);
|
|
||||||
}
|
function parseArgs(
|
||||||
return new FunctionCall(call.function, args);
|
parameters: FunctionCallParametersData,
|
||||||
|
): FunctionCallArgumentCollection {
|
||||||
|
return Object.keys(parameters || {})
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
||||||
|
.reduce((args, arg) => {
|
||||||
|
args.addArgument(arg);
|
||||||
|
return args;
|
||||||
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
for (const func of functions) {
|
return functions
|
||||||
const sharedFunction = parseFunction(func);
|
.map((func) => parseFunction(func))
|
||||||
collection.addFunction(sharedFunction);
|
.reduce((acc, func) => {
|
||||||
}
|
acc.addFunction(func);
|
||||||
return collection;
|
return acc;
|
||||||
|
}, collection);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,20 +41,21 @@ function parseFunction(data: FunctionData): ISharedFunction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
const parameters = new FunctionParameterCollection();
|
return (data.parameters || [])
|
||||||
if (!data.parameters) {
|
.map((parameter) => {
|
||||||
return parameters;
|
try {
|
||||||
}
|
return new FunctionParameter(
|
||||||
for (const parameterData of data.parameters) {
|
parameter.name,
|
||||||
const isOptional = parameterData.optional || false;
|
parameter.optional || false,
|
||||||
try {
|
);
|
||||||
const parameter = new FunctionParameter(parameterData.name, isOptional);
|
} catch (err) {
|
||||||
|
throw new Error(`"${data.name}": ${err.message}`);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.reduce((parameters, parameter) => {
|
||||||
parameters.addParameter(parameter);
|
parameters.addParameter(parameter);
|
||||||
} catch (err) {
|
return parameters;
|
||||||
throw new Error(`"${data.name}": ${err.message}`);
|
}, new FunctionParameterCollection());
|
||||||
}
|
|
||||||
}
|
|
||||||
return parameters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
function hasCode(data: FunctionData): boolean {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
|
import { getEnumValues, assertInRange } from '@/application/Common/Enum';
|
||||||
import { IEntity } from '../infrastructure/Entity/IEntity';
|
import { IEntity } from '../infrastructure/Entity/IEntity';
|
||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
import { IScript } from './IScript';
|
import { IScript } from './IScript';
|
||||||
@@ -57,16 +57,12 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
||||||
const totalOccurrencesById = new Map<TKey, number>();
|
const isUniqueInArray = (id: TKey, index: number, array: readonly TKey[]) => array
|
||||||
for (const entity of entities) {
|
.findIndex((otherId) => otherId === id) !== index;
|
||||||
totalOccurrencesById.set(entity.id, (totalOccurrencesById.get(entity.id) || 0) + 1);
|
const duplicatedIds = entities
|
||||||
}
|
.map((entity) => entity.id)
|
||||||
const duplicatedIds = new Array<TKey>();
|
.filter((id, index, array) => !isUniqueInArray(id, index, array))
|
||||||
totalOccurrencesById.forEach((index, id) => {
|
.filter(isUniqueInArray);
|
||||||
if (index > 1) {
|
|
||||||
duplicatedIds.push(id);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
if (duplicatedIds.length > 0) {
|
if (duplicatedIds.length > 0) {
|
||||||
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
const duplicatedIdsText = duplicatedIds.map((id) => `"${id}"`).join(',');
|
||||||
throw new Error(
|
throw new Error(
|
||||||
@@ -96,48 +92,37 @@ function ensureValidScripts(allScripts: readonly IScript[]) {
|
|||||||
if (!allScripts || allScripts.length === 0) {
|
if (!allScripts || allScripts.length === 0) {
|
||||||
throw new Error('must consist of at least one script');
|
throw new Error('must consist of at least one script');
|
||||||
}
|
}
|
||||||
for (const level of getEnumValues(RecommendationLevel)) {
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
if (allScripts.every((script) => script.level !== level)) {
|
.filter((level) => allScripts.every((script) => script.level !== level));
|
||||||
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
|
if (missingRecommendationLevels.length > 0) {
|
||||||
}
|
throw new Error('none of the scripts are recommended as'
|
||||||
|
+ ` "${missingRecommendationLevels.map((level) => RecommendationLevel[level]).join(', "')}".`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
|
function flattenApplication(
|
||||||
const allCategories = new Array<ICategory>();
|
|
||||||
const allScripts = new Array<IScript>();
|
|
||||||
flattenCategories(categories, allCategories, allScripts);
|
|
||||||
return [
|
|
||||||
allCategories,
|
|
||||||
allScripts,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
function flattenCategories(
|
|
||||||
categories: ReadonlyArray<ICategory>,
|
categories: ReadonlyArray<ICategory>,
|
||||||
allCategories: ICategory[],
|
): [ICategory[], IScript[]] {
|
||||||
allScripts: IScript[],
|
const [subCategories, subScripts] = (categories || [])
|
||||||
): IQueryableCollection {
|
// Parse children
|
||||||
if (!categories || categories.length === 0) {
|
.map((category) => flattenApplication(category.subCategories))
|
||||||
return;
|
// Flatten results
|
||||||
}
|
.reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => {
|
||||||
for (const category of categories) {
|
return [
|
||||||
allCategories.push(category);
|
[...previousCategories, ...currentCategories],
|
||||||
flattenScripts(category.scripts, allScripts);
|
[...previousScripts, ...currentScripts],
|
||||||
flattenCategories(category.subCategories, allCategories, allScripts);
|
];
|
||||||
}
|
}, [new Array<ICategory>(), new Array<IScript>()]);
|
||||||
}
|
return [
|
||||||
|
[
|
||||||
function flattenScripts(
|
...(categories || []),
|
||||||
scripts: ReadonlyArray<IScript>,
|
...subCategories,
|
||||||
allScripts: IScript[],
|
],
|
||||||
): IScript[] {
|
[
|
||||||
if (!scripts) {
|
...(categories || []).flatMap((category) => category.scripts || []),
|
||||||
return;
|
...subScripts,
|
||||||
}
|
],
|
||||||
for (const script of scripts) {
|
];
|
||||||
allScripts.push(script);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeQueryable(
|
function makeQueryable(
|
||||||
@@ -154,13 +139,15 @@ function makeQueryable(
|
|||||||
function groupByLevel(
|
function groupByLevel(
|
||||||
allScripts: readonly IScript[],
|
allScripts: readonly IScript[],
|
||||||
): Map<RecommendationLevel, readonly IScript[]> {
|
): Map<RecommendationLevel, readonly IScript[]> {
|
||||||
const map = new Map<RecommendationLevel, readonly IScript[]>();
|
return getEnumValues(RecommendationLevel)
|
||||||
for (const levelName of getEnumNames(RecommendationLevel)) {
|
.map((level) => ({
|
||||||
const level = RecommendationLevel[levelName];
|
level,
|
||||||
const scripts = allScripts.filter(
|
scripts: allScripts.filter(
|
||||||
(script) => script.level !== undefined && script.level <= level,
|
(script) => script.level !== undefined && script.level <= level,
|
||||||
);
|
),
|
||||||
map.set(level, scripts);
|
}))
|
||||||
}
|
.reduce((map, group) => {
|
||||||
return map;
|
map.set(group.level, group.scripts);
|
||||||
|
return map;
|
||||||
|
}, new Map<RecommendationLevel, readonly IScript[]>());
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class EventSource<T> implements IEventSource<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public notify(data: T) {
|
public notify(data: T) {
|
||||||
for (const handler of Array.from(this.handlers.values())) {
|
for (const handler of this.handlers.values()) {
|
||||||
handler(data);
|
handler(data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export class SelectionTypeHandler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getCurrentSelectionType(): SelectionType {
|
public getCurrentSelectionType(): SelectionType {
|
||||||
for (const [type, selector] of Array.from(selectors.entries())) {
|
for (const [type, selector] of selectors.entries()) {
|
||||||
if (selector.isSelected(this.state)) {
|
if (selector.isSelected(this.state)) {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,12 +3,7 @@ import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|||||||
import { INode, NodeType } from './SelectableTree/Node/INode';
|
import { INode, NodeType } from './SelectableTree/Node/INode';
|
||||||
|
|
||||||
export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined {
|
export function parseAllCategories(collection: ICategoryCollection): INode[] | undefined {
|
||||||
const nodes = new Array<INode>();
|
return createCategoryNodes(collection.actions);
|
||||||
for (const category of collection.actions) {
|
|
||||||
const children = parseCategoryRecursively(category);
|
|
||||||
nodes.push(convertCategoryToNode(category, children));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function parseSingleCategory(
|
export function parseSingleCategory(
|
||||||
@@ -43,31 +38,21 @@ function parseCategoryRecursively(
|
|||||||
if (!parentCategory) {
|
if (!parentCategory) {
|
||||||
throw new Error('parentCategory is undefined');
|
throw new Error('parentCategory is undefined');
|
||||||
}
|
}
|
||||||
let nodes = new Array<INode>();
|
return [
|
||||||
nodes = addCategories(parentCategory.subCategories, nodes);
|
...createCategoryNodes(parentCategory.subCategories),
|
||||||
nodes = addScripts(parentCategory.scripts, nodes);
|
...createScriptNodes(parentCategory.scripts),
|
||||||
return nodes;
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function addScripts(scripts: ReadonlyArray<IScript>, nodes: INode[]): INode[] {
|
function createScriptNodes(scripts: ReadonlyArray<IScript>): INode[] {
|
||||||
if (!scripts || scripts.length === 0) {
|
return (scripts || [])
|
||||||
return nodes;
|
.map((script) => convertScriptToNode(script));
|
||||||
}
|
|
||||||
for (const script of scripts) {
|
|
||||||
nodes.push(convertScriptToNode(script));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addCategories(categories: ReadonlyArray<ICategory>, nodes: INode[]): INode[] {
|
function createCategoryNodes(categories: ReadonlyArray<ICategory>): INode[] {
|
||||||
if (!categories || categories.length === 0) {
|
return (categories || [])
|
||||||
return nodes;
|
.map((category) => ({ category, children: parseCategoryRecursively(category) }))
|
||||||
}
|
.map((data) => convertCategoryToNode(data.category, data.children));
|
||||||
for (const category of categories) {
|
|
||||||
const subCategoryNodes = parseCategoryRecursively(category);
|
|
||||||
nodes.push(convertCategoryToNode(category, subCategoryNodes));
|
|
||||||
}
|
|
||||||
return nodes;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function convertCategoryToNode(
|
function convertCategoryToNode(
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ function testExpectedInstanceTypes<T>(
|
|||||||
) {
|
) {
|
||||||
describe('create returns expected instances', () => {
|
describe('create returns expected instances', () => {
|
||||||
// arrange
|
// arrange
|
||||||
for (const language of Array.from(expectedTypes.keys())) {
|
for (const language of expectedTypes.keys()) {
|
||||||
it(ScriptingLanguage[language], () => {
|
it(ScriptingLanguage[language], () => {
|
||||||
// act
|
// act
|
||||||
const expected = expectedTypes.get(language);
|
const expected = expectedTypes.get(language);
|
||||||
|
|||||||
@@ -197,7 +197,7 @@ class UserScriptGeneratorMock implements IUserScriptGenerator {
|
|||||||
selectedScripts: readonly SelectedScript[],
|
selectedScripts: readonly SelectedScript[],
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
for (const [parameters, result] of Array.from(this.prePlanned)) {
|
for (const [parameters, result] of this.prePlanned) {
|
||||||
if (selectedScripts === parameters.scripts
|
if (selectedScripts === parameters.scripts
|
||||||
&& scriptingDefinition === parameters.definition) {
|
&& scriptingDefinition === parameters.definition) {
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@@ -13,16 +13,23 @@ describe('CodeChangedEvent', () => {
|
|||||||
it('throws when code position is out of range', () => {
|
it('throws when code position is out of range', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const code = 'singleline code';
|
const code = 'singleline code';
|
||||||
|
const nonExistingLine1 = 2;
|
||||||
|
const nonExistingLine2 = 31;
|
||||||
const newScripts = new Map<SelectedScript, ICodePosition>([
|
const newScripts = new Map<SelectedScript, ICodePosition>([
|
||||||
[new SelectedScriptStub('1'), new CodePosition(0, 2 /* nonexisting line */)],
|
[new SelectedScriptStub('1'), new CodePosition(0, nonExistingLine1)],
|
||||||
|
[new SelectedScriptStub('2'), new CodePosition(0, nonExistingLine2)],
|
||||||
]);
|
]);
|
||||||
// act
|
// act
|
||||||
const act = () => new CodeChangedEventBuilder()
|
let errorText = '';
|
||||||
.withCode(code)
|
try {
|
||||||
.withNewScripts(newScripts)
|
new CodeChangedEventBuilder()
|
||||||
.build();
|
.withCode(code)
|
||||||
|
.withNewScripts(newScripts)
|
||||||
|
.build();
|
||||||
|
} catch (error) { errorText = error.message; }
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw();
|
expect(errorText).to.include(nonExistingLine1);
|
||||||
|
expect(errorText).to.include(nonExistingLine2);
|
||||||
});
|
});
|
||||||
describe('does not throw with valid code position', () => {
|
describe('does not throw with valid code position', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
|||||||
@@ -151,6 +151,14 @@ describe('ExpressionsCompiler', () => {
|
|||||||
.withArgument('parameter2', 'value'),
|
.withArgument('parameter2', 'value'),
|
||||||
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter3" but used in code',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: 'parameter names are not repeated in error message',
|
||||||
|
expressions: [
|
||||||
|
new ExpressionStub().withParameterNames(['parameter1', 'parameter1', 'parameter2', 'parameter2'], false),
|
||||||
|
],
|
||||||
|
args: new FunctionCallArgumentCollectionStub(),
|
||||||
|
expectedError: 'parameter value(s) not provided for: "parameter1", "parameter2" but used in code',
|
||||||
|
},
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
for (const testCase of testCases) {
|
||||||
it(testCase.name, () => {
|
it(testCase.name, () => {
|
||||||
|
|||||||
@@ -31,6 +31,31 @@ describe('RegexParser', () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
it('throws when position is invalid', () => {
|
||||||
|
// arrange
|
||||||
|
const regexMatchingEmpty = /^/gm; /* expressions cannot be empty */
|
||||||
|
const code = 'unimportant';
|
||||||
|
const expectedErrorParts = [
|
||||||
|
`[${RegexParserConcrete.constructor.name}]`,
|
||||||
|
'invalid script position',
|
||||||
|
`Regex: ${regexMatchingEmpty}`,
|
||||||
|
`Code: ${code}`,
|
||||||
|
];
|
||||||
|
const sut = new RegexParserConcrete(regexMatchingEmpty);
|
||||||
|
// act
|
||||||
|
let error: string;
|
||||||
|
try {
|
||||||
|
sut.findExpressions(code);
|
||||||
|
} catch (err) {
|
||||||
|
error = err.message;
|
||||||
|
}
|
||||||
|
// assert
|
||||||
|
expect(
|
||||||
|
expectedErrorParts.every((part) => error.includes(part)),
|
||||||
|
`Expected parts: ${expectedErrorParts.join(', ')}`
|
||||||
|
+ `Actual error: ${error}`,
|
||||||
|
);
|
||||||
|
});
|
||||||
describe('matches regex as expected', () => {
|
describe('matches regex as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases = [
|
const testCases = [
|
||||||
|
|||||||
@@ -70,7 +70,7 @@ describe('FunctionCallCompiler', () => {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'provided: an unexpected parameter, when: none required',
|
name: 'provided: an unexpected parameter, when: none required',
|
||||||
functionParameters: undefined,
|
functionParameters: [],
|
||||||
callParameters: ['unexpected-call-parameter'],
|
callParameters: ['unexpected-call-parameter'],
|
||||||
expectedError:
|
expectedError:
|
||||||
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
|
`Function "${functionName}" has unexpected parameter(s) provided: "unexpected-call-parameter"`
|
||||||
@@ -90,10 +90,10 @@ describe('FunctionCallCompiler', () => {
|
|||||||
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
const func = new SharedFunctionStub(FunctionBodyType.Code)
|
||||||
.withName('test-function-name')
|
.withName('test-function-name')
|
||||||
.withParameterNames(...testCase.functionParameters);
|
.withParameterNames(...testCase.functionParameters);
|
||||||
let params: FunctionCallParametersData = {};
|
const params = testCase.callParameters
|
||||||
for (const parameter of testCase.callParameters) {
|
.reduce((result, parameter) => {
|
||||||
params = { ...params, [parameter]: 'defined-parameter-value ' };
|
return { ...result, [parameter]: 'defined-parameter-value ' };
|
||||||
}
|
}, {} as FunctionCallParametersData);
|
||||||
const call = new FunctionCallStub()
|
const call = new FunctionCallStub()
|
||||||
.withFunctionName(func.name)
|
.withFunctionName(func.name)
|
||||||
.withArguments(params);
|
.withArguments(params);
|
||||||
|
|||||||
@@ -49,15 +49,11 @@ async function findBadLineNumbers(fileContent: string): Promise<number[]> {
|
|||||||
|
|
||||||
function findLineNumbersEndingWith(content: string, ending: string): number[] {
|
function findLineNumbersEndingWith(content: string, ending: string): number[] {
|
||||||
sanityCheck(content, ending);
|
sanityCheck(content, ending);
|
||||||
const lines = content.split(/\r\n|\r|\n/);
|
return content
|
||||||
const results = new Array<number>();
|
.split(/\r\n|\r|\n/)
|
||||||
for (let i = 0; i < lines.length; i++) {
|
.map((line, index) => ({ text: line, index }))
|
||||||
const line = lines[i];
|
.filter((line) => line.text.trim().endsWith(ending))
|
||||||
if (line.trim().endsWith(ending)) {
|
.map((line) => line.index + 1 /* first line is 1, not 0 */);
|
||||||
results.push((i + 1 /* first line is 1 not 0 */));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return results;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sanityCheck(content: string, ending: string): void {
|
function sanityCheck(content: string, ending: string): void {
|
||||||
|
|||||||
@@ -121,24 +121,45 @@ describe('CategoryCollection', () => {
|
|||||||
expect(construct).to.throw('must consist of at least one script');
|
expect(construct).to.throw('must consist of at least one script');
|
||||||
});
|
});
|
||||||
describe('cannot construct without any recommended scripts', () => {
|
describe('cannot construct without any recommended scripts', () => {
|
||||||
// arrange
|
describe('single missing', () => {
|
||||||
const recommendationLevels = getEnumValues(RecommendationLevel);
|
// arrange
|
||||||
for (const missingLevel of recommendationLevels) {
|
const recommendationLevels = getEnumValues(RecommendationLevel);
|
||||||
it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => {
|
for (const missingLevel of recommendationLevels) {
|
||||||
const expectedError = `none of the scripts are recommended as ${RecommendationLevel[missingLevel]}`;
|
it(`when "${RecommendationLevel[missingLevel]}" is missing`, () => {
|
||||||
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
|
const expectedError = `none of the scripts are recommended as "${RecommendationLevel[missingLevel]}".`;
|
||||||
const categories = otherLevels.map(
|
const otherLevels = recommendationLevels.filter((level) => level !== missingLevel);
|
||||||
(level, index) => new CategoryStub(index)
|
const categories = otherLevels.map(
|
||||||
.withScript(new ScriptStub(`Script${index}`).withLevel(level)),
|
(level, index) => new CategoryStub(index)
|
||||||
);
|
.withScript(
|
||||||
// act
|
new ScriptStub(`Script${index}`).withLevel(level),
|
||||||
const construct = () => new CategoryCollectionBuilder()
|
),
|
||||||
.withActions(categories)
|
);
|
||||||
.construct();
|
// act
|
||||||
// assert
|
const construct = () => new CategoryCollectionBuilder()
|
||||||
expect(construct).to.throw(expectedError);
|
.withActions(categories)
|
||||||
});
|
.construct();
|
||||||
}
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
it('multiple are missing', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedError = 'none of the scripts are recommended as '
|
||||||
|
+ `"${RecommendationLevel[RecommendationLevel.Standard]}, "${RecommendationLevel[RecommendationLevel.Strict]}".`;
|
||||||
|
const categories = [
|
||||||
|
new CategoryStub(0)
|
||||||
|
.withScript(
|
||||||
|
new ScriptStub(`Script${0}`).withLevel(undefined),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
// act
|
||||||
|
const construct = () => new CategoryCollectionBuilder()
|
||||||
|
.withActions(categories)
|
||||||
|
.construct();
|
||||||
|
// assert
|
||||||
|
expect(construct).to.throw(expectedError);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('totalScripts', () => {
|
describe('totalScripts', () => {
|
||||||
|
|||||||
@@ -61,25 +61,24 @@ describe('ScriptCode', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
const actions = [];
|
const actions = testCases.flatMap((testCase) => ([
|
||||||
for (const testCase of testCases) {
|
{
|
||||||
actions.push(...[
|
act: () => new ScriptCodeBuilder()
|
||||||
{
|
.withExecute(testCase.code)
|
||||||
act: () => new ScriptCodeBuilder()
|
.build(),
|
||||||
.withExecute(testCase.code)
|
testName: `execute: ${testCase.testName}`,
|
||||||
.build(),
|
expectedMessage: testCase.expectedMessage,
|
||||||
testName: `execute: ${testCase.testName}`,
|
code: testCase.code,
|
||||||
expectedMessage: testCase.expectedMessage,
|
},
|
||||||
},
|
{
|
||||||
{
|
act: () => new ScriptCodeBuilder()
|
||||||
act: () => new ScriptCodeBuilder()
|
.withRevert(testCase.code)
|
||||||
.withRevert(testCase.code)
|
.build(),
|
||||||
.build(),
|
testName: `revert: ${testCase.testName}`,
|
||||||
testName: `revert: ${testCase.testName}`,
|
expectedMessage: `(revert): ${testCase.expectedMessage}`,
|
||||||
expectedMessage: `(revert): ${testCase.expectedMessage}`,
|
code: testCase.code,
|
||||||
},
|
},
|
||||||
]);
|
]));
|
||||||
}
|
|
||||||
// assert
|
// assert
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
it(action.testName, () => {
|
it(action.testName, () => {
|
||||||
@@ -115,27 +114,24 @@ describe('ScriptCode', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
const actions = [];
|
const actions = testCases.flatMap((testCase) => ([
|
||||||
for (const testCase of testCases) {
|
{
|
||||||
actions.push(...[
|
testName: `execute: ${testCase.testName}`,
|
||||||
{
|
act: () => new ScriptCodeBuilder()
|
||||||
testName: `execute: ${testCase.testName}`,
|
.withSyntax(syntax)
|
||||||
act: () => new ScriptCodeBuilder()
|
.withExecute(testCase.code)
|
||||||
.withSyntax(syntax)
|
.build(),
|
||||||
.withExecute(testCase.code)
|
expect: (sut: IScriptCode) => sut.execute === testCase.code,
|
||||||
.build(),
|
},
|
||||||
expect: (sut: IScriptCode) => sut.execute === testCase.code,
|
{
|
||||||
},
|
testName: `revert: ${testCase.testName}`,
|
||||||
{
|
act: () => new ScriptCodeBuilder()
|
||||||
testName: `revert: ${testCase.testName}`,
|
.withSyntax(syntax)
|
||||||
act: () => new ScriptCodeBuilder()
|
.withRevert(testCase.code)
|
||||||
.withSyntax(syntax)
|
.build(),
|
||||||
.withRevert(testCase.code)
|
expect: (sut: IScriptCode) => sut.revert === testCase.code,
|
||||||
.build(),
|
},
|
||||||
expect: (sut: IScriptCode) => sut.revert === testCase.code,
|
]));
|
||||||
},
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
// assert
|
// assert
|
||||||
for (const action of actions) {
|
for (const action of actions) {
|
||||||
it(action.testName, () => {
|
it(action.testName, () => {
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe('ScriptingDefinition', () => {
|
|||||||
[ScriptingLanguage.batchfile, 'bat'],
|
[ScriptingLanguage.batchfile, 'bat'],
|
||||||
[ScriptingLanguage.shellscript, 'sh'],
|
[ScriptingLanguage.shellscript, 'sh'],
|
||||||
]);
|
]);
|
||||||
Array.from(testCases.entries()).forEach((test) => {
|
for (const test of testCases.entries()) {
|
||||||
const language = test[0];
|
const language = test[0];
|
||||||
const expectedExtension = test[1];
|
const expectedExtension = test[1];
|
||||||
it(`${ScriptingLanguage[language]} has ${expectedExtension}`, () => {
|
it(`${ScriptingLanguage[language]} has ${expectedExtension}`, () => {
|
||||||
@@ -50,7 +50,7 @@ describe('ScriptingDefinition', () => {
|
|||||||
// assert
|
// assert
|
||||||
expect(sut.fileExtension, expectedExtension);
|
expect(sut.fileExtension, expectedExtension);
|
||||||
});
|
});
|
||||||
});
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('startCode', () => {
|
describe('startCode', () => {
|
||||||
|
|||||||
@@ -52,14 +52,12 @@ class SchedulerMock {
|
|||||||
|
|
||||||
public tickNext(ms: number) {
|
public tickNext(ms: number) {
|
||||||
const newTime = this.currentTime + ms;
|
const newTime = this.currentTime + ms;
|
||||||
let newActions = this.scheduledActions;
|
const dueActions = this.scheduledActions
|
||||||
for (const action of this.scheduledActions) {
|
.filter((action) => newTime >= action.time);
|
||||||
if (newTime >= action.time) {
|
for (const action of dueActions) {
|
||||||
newActions = newActions.filter((a) => a !== action);
|
action.action();
|
||||||
action.action();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
this.scheduledActions = newActions;
|
this.scheduledActions = this.scheduledActions.filter((action) => !dueActions.includes(action));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ describe('ScriptNodeParser', () => {
|
|||||||
expectSameScript(nodes[2], scripts[2]);
|
expectSameScript(nodes[2], scripts[2]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('parseAllCategories parses as expected', () => {
|
it('parseAllCategories parses as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const collection = new CategoryCollectionStub()
|
const collection = new CategoryCollectionStub()
|
||||||
|
|||||||
@@ -61,46 +61,27 @@ export class CategoryCollectionStub implements ICategoryCollection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): ReadonlyArray<IScript> {
|
public getAllScripts(): ReadonlyArray<IScript> {
|
||||||
const scripts = [];
|
return this.actions.flatMap((category) => getScriptsRecursively(category));
|
||||||
for (const category of this.actions) {
|
|
||||||
const categoryScripts = getScriptsRecursively(category);
|
|
||||||
scripts.push(...categoryScripts);
|
|
||||||
}
|
|
||||||
return scripts;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllCategories(): ReadonlyArray<ICategory> {
|
public getAllCategories(): ReadonlyArray<ICategory> {
|
||||||
const categories = [];
|
return this.actions.flatMap(
|
||||||
categories.push(...this.actions);
|
(category) => [category, ...getSubCategoriesRecursively(category)],
|
||||||
for (const category of this.actions) {
|
);
|
||||||
const subCategories = getSubCategoriesRecursively(category);
|
|
||||||
categories.push(...subCategories);
|
|
||||||
}
|
|
||||||
return categories;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> {
|
function getSubCategoriesRecursively(category: ICategory): ReadonlyArray<ICategory> {
|
||||||
const subCategories = [];
|
return (category.subCategories || []).flatMap(
|
||||||
if (category.subCategories) {
|
(subCategory) => [subCategory, ...getSubCategoriesRecursively(subCategory)],
|
||||||
for (const subCategory of category.subCategories) {
|
);
|
||||||
subCategories.push(subCategory);
|
|
||||||
subCategories.push(...getSubCategoriesRecursively(subCategory));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return subCategories;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
function getScriptsRecursively(category: ICategory): ReadonlyArray<IScript> {
|
||||||
const categoryScripts = [];
|
return [
|
||||||
if (category.scripts) {
|
...(category.scripts || []),
|
||||||
categoryScripts.push(...category.scripts);
|
...(category.subCategories || []).flatMap(
|
||||||
}
|
(subCategory) => getScriptsRecursively(subCategory),
|
||||||
if (category.subCategories) {
|
),
|
||||||
for (const subCategory of category.subCategories) {
|
];
|
||||||
const subCategoryScripts = getScriptsRecursively(subCategory);
|
|
||||||
categoryScripts.push(...subCategoryScripts);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return categoryScripts;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,10 +27,9 @@ export class CategoryStub extends BaseEntity<number> implements ICategory {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
public withScriptIds(...scriptIds: string[]): CategoryStub {
|
||||||
for (const scriptId of scriptIds) {
|
return this.withScripts(
|
||||||
this.withScript(new ScriptStub(scriptId));
|
...scriptIds.map((id) => new ScriptStub(id)),
|
||||||
}
|
);
|
||||||
return this;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public withScripts(...scripts: IScript[]): CategoryStub {
|
public withScripts(...scripts: IScript[]): CategoryStub {
|
||||||
|
|||||||
@@ -58,12 +58,9 @@ function deepEqual(
|
|||||||
if (!scrambledEqual(expectedParameterNames, actualParameterNames)) {
|
if (!scrambledEqual(expectedParameterNames, actualParameterNames)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
for (const parameterName of expectedParameterNames) {
|
return expectedParameterNames.every((parameterName) => {
|
||||||
const expectedValue = expected.getArgument(parameterName).argumentValue;
|
const expectedValue = expected.getArgument(parameterName).argumentValue;
|
||||||
const actualValue = actual.getArgument(parameterName).argumentValue;
|
const actualValue = actual.getArgument(parameterName).argumentValue;
|
||||||
if (expectedValue !== actualValue) {
|
return expectedValue === actualValue;
|
||||||
return false;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,9 +14,8 @@ export class FunctionCallArgumentCollectionStub implements IFunctionCallArgument
|
|||||||
}
|
}
|
||||||
|
|
||||||
public withArguments(args: { readonly [index: string]: string }) {
|
public withArguments(args: { readonly [index: string]: string }) {
|
||||||
for (const parameterName of Object.keys(args)) {
|
for (const [name, value] of Object.entries(args)) {
|
||||||
const parameterValue = args[parameterName];
|
this.withArgument(name, value);
|
||||||
this.withArgument(parameterName, parameterValue);
|
|
||||||
}
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
|
|||||||
|
|
||||||
public readonly documentationUrls = new Array<string>();
|
public readonly documentationUrls = new Array<string>();
|
||||||
|
|
||||||
public level = RecommendationLevel.Standard;
|
public level? = RecommendationLevel.Standard;
|
||||||
|
|
||||||
constructor(public readonly id: string) {
|
constructor(public readonly id: string) {
|
||||||
super(id);
|
super(id);
|
||||||
@@ -23,7 +23,7 @@ export class ScriptStub extends BaseEntity<string> implements IScript {
|
|||||||
return Boolean(this.code.revert);
|
return Boolean(this.code.revert);
|
||||||
}
|
}
|
||||||
|
|
||||||
public withLevel(value: RecommendationLevel): ScriptStub {
|
public withLevel(value?: RecommendationLevel): ScriptStub {
|
||||||
this.level = value;
|
this.level = value;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
"module": "esnext",
|
"module": "esnext",
|
||||||
"jsx": "preserve",
|
"jsx": "preserve",
|
||||||
"importHelpers": true,
|
"importHelpers": true,
|
||||||
|
"downlevelIteration": true,
|
||||||
"moduleResolution": "node",
|
"moduleResolution": "node",
|
||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user