Add object property validation in parser #369
This commit introduces stricter type validation across the application to reject objects with unexpected properties, enhancing the robustness and predictability of data handling. Changes include: - Implement a common utility to validate object types. - Refactor across various parsers and data handlers to utilize the new validations. - Update error messages for better clarity and troubleshooting.
This commit is contained in:
@@ -5,13 +5,13 @@ export type EnumType = number | string;
|
|||||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
= { [key in T]: TEnumValue };
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface IEnumParser<TEnum> {
|
export interface EnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): IEnumParser<TEnumValue> {
|
): EnumParser<TEnumValue> {
|
||||||
return {
|
return {
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,40 +1,48 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
|
||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
categoryParser = parseCategoryCollection,
|
collectionsData: readonly CollectionData[] = PreParsedCollections,
|
||||||
projectDetailsParser = parseProjectDetails,
|
utilities: ApplicationParserUtilities = DefaultUtilities,
|
||||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
|
||||||
collectionsData = PreParsedCollections,
|
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData);
|
validateCollectionsData(collectionsData, utilities.validator);
|
||||||
const projectDetails = projectDetailsParser(metadata);
|
const projectDetails = utilities.parseProjectDetails();
|
||||||
const collections = collectionsData.map(
|
const collections = collectionsData.map(
|
||||||
(collection) => categoryParser(collection, projectDetails),
|
(collection) => utilities.parseCategoryCollection(collection, projectDetails),
|
||||||
);
|
);
|
||||||
const app = new Application(projectDetails, collections);
|
const app = new Application(projectDetails, collections);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryCollectionParserType
|
|
||||||
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
|
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData[] = [
|
const PreParsedCollections: readonly CollectionData[] = [
|
||||||
WindowsData, MacOsData, LinuxData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(
|
||||||
if (!collections.length) {
|
collections: readonly CollectionData[],
|
||||||
throw new Error('missing collections');
|
validator: TypeValidator,
|
||||||
|
) {
|
||||||
|
validator.assertNonEmptyCollection({
|
||||||
|
value: collections,
|
||||||
|
valueName: 'collections',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ApplicationParserUtilities {
|
||||||
|
readonly parseCategoryCollection: CategoryCollectionParser;
|
||||||
|
readonly validator: TypeValidator;
|
||||||
|
readonly parseProjectDetails: ProjectDetailsParser;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: ApplicationParserUtilities = {
|
||||||
|
parseCategoryCollection,
|
||||||
|
parseProjectDetails,
|
||||||
|
validator: createTypeValidator(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -3,33 +3,73 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser } from '../Common/Enum';
|
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||||
import { parseCategory } from './Executable/CategoryParser';
|
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||||
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
||||||
|
|
||||||
export function parseCategoryCollection(
|
export const parseCategoryCollection: CategoryCollectionParser = (
|
||||||
|
content,
|
||||||
|
projectDetails,
|
||||||
|
utilities: CategoryCollectionParserUtilities = DefaultUtilities,
|
||||||
|
) => {
|
||||||
|
validateCollection(content, utilities.validator);
|
||||||
|
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
||||||
|
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
|
||||||
|
const categories = content.actions.map(
|
||||||
|
(action) => utilities.parseCategory(action, collectionUtilities),
|
||||||
|
);
|
||||||
|
const os = utilities.osParser.parseEnum(content.os, 'os');
|
||||||
|
const collection = utilities.createCategoryCollection({
|
||||||
|
os, actions: categories, scripting,
|
||||||
|
});
|
||||||
|
return collection;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type CategoryCollectionFactory = (
|
||||||
|
...parameters: ConstructorParameters<typeof CategoryCollection>
|
||||||
|
) => ICategoryCollection;
|
||||||
|
|
||||||
|
export interface CategoryCollectionParser {
|
||||||
|
(
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
projectDetails: ProjectDetails,
|
projectDetails: ProjectDetails,
|
||||||
osParser = createEnumParser(OperatingSystem),
|
utilities?: CategoryCollectionParserUtilities,
|
||||||
createUtilities: CategoryCollectionSpecificUtilitiesFactory = createCollectionUtilities,
|
): ICategoryCollection;
|
||||||
): ICategoryCollection {
|
|
||||||
validate(content);
|
|
||||||
const scripting = new ScriptingDefinitionParser()
|
|
||||||
.parse(content.scripting, projectDetails);
|
|
||||||
const utilities = createUtilities(content.functions, scripting);
|
|
||||||
const categories = content.actions.map((action) => parseCategory(action, utilities));
|
|
||||||
const os = osParser.parseEnum(content.os, 'os');
|
|
||||||
const collection = new CategoryCollection(
|
|
||||||
os,
|
|
||||||
categories,
|
|
||||||
scripting,
|
|
||||||
);
|
|
||||||
return collection;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validateCollection(
|
||||||
if (!content.actions.length) {
|
content: CollectionData,
|
||||||
throw new Error('content does not define any action');
|
validator: TypeValidator,
|
||||||
|
): void {
|
||||||
|
validator.assertObject({
|
||||||
|
value: content,
|
||||||
|
valueName: 'collection',
|
||||||
|
allowedProperties: [
|
||||||
|
'os', 'scripting', 'actions', 'functions',
|
||||||
|
],
|
||||||
|
});
|
||||||
|
validator.assertNonEmptyCollection({
|
||||||
|
value: content.actions,
|
||||||
|
valueName: '"actions" in collection',
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface CategoryCollectionParserUtilities {
|
||||||
|
readonly osParser: EnumParser<OperatingSystem>;
|
||||||
|
readonly validator: TypeValidator;
|
||||||
|
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
||||||
|
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
|
||||||
|
readonly parseCategory: CategoryParser;
|
||||||
|
readonly createCategoryCollection: CategoryCollectionFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: CategoryCollectionParserUtilities = {
|
||||||
|
osParser: createEnumParser(OperatingSystem),
|
||||||
|
validator: createTypeValidator(),
|
||||||
|
parseScriptingDefinition,
|
||||||
|
createUtilities: createCollectionUtilities,
|
||||||
|
parseCategory,
|
||||||
|
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
||||||
|
};
|
||||||
|
|||||||
96
src/application/Parser/Common/TypeValidator.ts
Normal file
96
src/application/Parser/Common/TypeValidator.ts
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
import type { PropertyKeys } from '@/TypeHelpers';
|
||||||
|
import { isNullOrUndefined, isArray, isPlainObject } from '@/TypeHelpers';
|
||||||
|
|
||||||
|
export interface TypeValidator {
|
||||||
|
assertObject<T>(assertion: ObjectAssertion<T>): void;
|
||||||
|
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NonEmptyCollectionAssertion {
|
||||||
|
readonly value: unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ObjectAssertion<T> {
|
||||||
|
readonly value: T | unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
readonly allowedProperties?: readonly PropertyKeys<T>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function createTypeValidator(): TypeValidator {
|
||||||
|
return {
|
||||||
|
assertObject: (assertion) => {
|
||||||
|
assertDefined(assertion.value, assertion.valueName);
|
||||||
|
assertPlainObject(assertion.value, assertion.valueName);
|
||||||
|
assertNoEmptyProperties(assertion.value, assertion.valueName);
|
||||||
|
if (assertion.allowedProperties !== undefined) {
|
||||||
|
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
|
||||||
|
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
assertNonEmptyCollection: (assertion) => {
|
||||||
|
assertDefined(assertion.value, assertion.valueName);
|
||||||
|
assertArray(assertion.value, assertion.valueName);
|
||||||
|
assertNonEmpty(assertion.value, assertion.valueName);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertDefined<T>(
|
||||||
|
value: T,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is NonNullable<T> {
|
||||||
|
if (isNullOrUndefined(value)) {
|
||||||
|
throw new Error(`'${valueName}' is missing.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertPlainObject(
|
||||||
|
value: unknown,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is object {
|
||||||
|
if (!isPlainObject(value)) {
|
||||||
|
throw new Error(`'${valueName}' is not an object.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNoEmptyProperties(
|
||||||
|
value: object,
|
||||||
|
valueName: string,
|
||||||
|
): void {
|
||||||
|
if (Object.keys(value).length === 0) {
|
||||||
|
throw new Error(`'${valueName}' is an empty object without properties.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertAllowedProperties(
|
||||||
|
value: object,
|
||||||
|
valueName: string,
|
||||||
|
allowedProperties: readonly string[],
|
||||||
|
): void {
|
||||||
|
const properties = Object.keys(value).map((p) => p as string);
|
||||||
|
const disallowedProperties = properties.filter(
|
||||||
|
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
|
||||||
|
);
|
||||||
|
if (disallowedProperties.length > 0) {
|
||||||
|
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertArray(
|
||||||
|
value: unknown,
|
||||||
|
valueName: string,
|
||||||
|
): asserts value is Array<unknown> {
|
||||||
|
if (!isArray(value)) {
|
||||||
|
throw new Error(`'${valueName}' should be of type 'array', but is of type '${typeof value}'.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertNonEmpty(
|
||||||
|
value: Array<unknown>,
|
||||||
|
valueName: string,
|
||||||
|
): void {
|
||||||
|
if (value.length === 0) {
|
||||||
|
throw new Error(`'${valueName}' cannot be an empty array.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import type {
|
import type {
|
||||||
CategoryData, ScriptData, ExecutableData,
|
CategoryData, ScriptData, ExecutableData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
import { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
@@ -13,16 +13,24 @@ import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSp
|
|||||||
|
|
||||||
let categoryIdCounter = 0;
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
export function parseCategory(
|
export const parseCategory: CategoryParser = (
|
||||||
category: CategoryData,
|
category: CategoryData,
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
): Category {
|
) => {
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
collectionUtilities,
|
collectionUtilities,
|
||||||
categoryUtilities,
|
categoryUtilities,
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategoryParser {
|
||||||
|
(
|
||||||
|
category: CategoryData,
|
||||||
|
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||||
|
categoryUtilities?: CategoryParserUtilities,
|
||||||
|
): Category;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryParseContext {
|
interface CategoryParseContext {
|
||||||
@@ -41,7 +49,7 @@ function parseCategoryRecursively(
|
|||||||
subscripts: new Array<Script>(),
|
subscripts: new Array<Script>(),
|
||||||
};
|
};
|
||||||
for (const data of context.categoryData.children) {
|
for (const data of context.categoryData.children) {
|
||||||
parseExecutable({
|
parseUnknownExecutable({
|
||||||
data,
|
data,
|
||||||
children,
|
children,
|
||||||
parent: context.categoryData,
|
parent: context.categoryData,
|
||||||
@@ -74,12 +82,18 @@ function ensureValidCategory(
|
|||||||
self: context.categoryData,
|
self: context.categoryData,
|
||||||
parentCategory: context.parentCategory,
|
parentCategory: context.parentCategory,
|
||||||
});
|
});
|
||||||
validator.assertDefined(category);
|
validator.assertType((v) => v.assertObject({
|
||||||
|
value: category,
|
||||||
|
valueName: category.category ?? 'category',
|
||||||
|
allowedProperties: [
|
||||||
|
'docs', 'children', 'category',
|
||||||
|
],
|
||||||
|
}));
|
||||||
validator.assertValidName(category.category);
|
validator.assertValidName(category.category);
|
||||||
validator.assert(
|
validator.assertType((v) => v.assertNonEmptyCollection({
|
||||||
() => Boolean(category.children) && category.children.length > 0,
|
value: category.children,
|
||||||
`"${category.category}" has no children.`,
|
valueName: category.category,
|
||||||
);
|
}));
|
||||||
return validator;
|
return validator;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,12 +110,15 @@ interface ExecutableParseContext {
|
|||||||
readonly categoryUtilities: CategoryParserUtilities;
|
readonly categoryUtilities: CategoryParserUtilities;
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseExecutable(context: ExecutableParseContext) {
|
function parseUnknownExecutable(context: ExecutableParseContext) {
|
||||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||||
self: context.data,
|
self: context.data,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
});
|
});
|
||||||
validator.assertDefined(context.data);
|
validator.assertType((v) => v.assertObject({
|
||||||
|
value: context.data,
|
||||||
|
valueName: 'Executable',
|
||||||
|
}));
|
||||||
validator.assert(
|
validator.assert(
|
||||||
() => isCategory(context.data) || isScript(context.data),
|
() => isCategory(context.data) || isScript(context.data),
|
||||||
'Executable is neither a category or a script.',
|
'Executable is neither a category or a script.',
|
||||||
|
|||||||
@@ -50,5 +50,5 @@ class DocumentationContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function throwInvalidType(): never {
|
function throwInvalidType(): never {
|
||||||
throw new Error('docs field (documentation) must be an array of strings');
|
throw new Error('docs field (documentation) must be a single string or an array of strings.');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||||
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import type { IExpressionsCompiler } from '@/application/Parser/Executable/Scrip
|
|||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
|
import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import type { ArgumentCompiler } from './ArgumentCompiler';
|
import type { ArgumentCompiler } from './ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import {
|
|||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
|
||||||
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
import type { ArgumentCompiler } from './Argument/ArgumentCompiler';
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/
|
|||||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
|
||||||
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
import { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||||
import { FunctionParameter } from './Parameter/FunctionParameter';
|
import { FunctionParameter } from './Parameter/FunctionParameter';
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Val
|
|||||||
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
import { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
||||||
|
|||||||
@@ -5,11 +5,11 @@ import { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
|
|||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { Script } from '@/domain/Executables/Script/Script';
|
||||||
import { createEnumParser, type IEnumParser } from '@/application/Common/Enum';
|
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
||||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
||||||
import { ExecutableType } from '../Validation/ExecutableType';
|
import { ExecutableType } from '../Validation/ExecutableType';
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||||
@@ -28,23 +28,28 @@ export interface ScriptParser {
|
|||||||
export const parseScript: ScriptParser = (
|
export const parseScript: ScriptParser = (
|
||||||
data,
|
data,
|
||||||
collectionUtilities,
|
collectionUtilities,
|
||||||
utilities = DefaultScriptParserUtilities,
|
scriptUtilities = DefaultUtilities,
|
||||||
) => {
|
) => {
|
||||||
const validator = utilities.createValidator({
|
const validator = scriptUtilities.createValidator({
|
||||||
type: ExecutableType.Script,
|
type: ExecutableType.Script,
|
||||||
self: data,
|
self: data,
|
||||||
});
|
});
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
try {
|
try {
|
||||||
const script = utilities.createScript({
|
const script = scriptUtilities.createScript({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
code: parseCode(data, collectionUtilities, utilities.codeValidator, utilities.createCode),
|
code: parseCode(
|
||||||
docs: utilities.parseDocs(data),
|
data,
|
||||||
level: parseLevel(data.recommend, utilities.levelParser),
|
collectionUtilities,
|
||||||
|
scriptUtilities.codeValidator,
|
||||||
|
scriptUtilities.createCode,
|
||||||
|
),
|
||||||
|
docs: scriptUtilities.parseDocs(data),
|
||||||
|
level: parseLevel(data.recommend, scriptUtilities.levelParser),
|
||||||
});
|
});
|
||||||
return script;
|
return script;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw utilities.wrapError(
|
throw scriptUtilities.wrapError(
|
||||||
error,
|
error,
|
||||||
validator.createContextualErrorMessage('Failed to parse script.'),
|
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||||
);
|
);
|
||||||
@@ -53,7 +58,7 @@ export const parseScript: ScriptParser = (
|
|||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string | undefined,
|
level: string | undefined,
|
||||||
parser: IEnumParser<RecommendationLevel>,
|
parser: EnumParser<RecommendationLevel>,
|
||||||
): RecommendationLevel | undefined {
|
): RecommendationLevel | undefined {
|
||||||
if (!level) {
|
if (!level) {
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -95,7 +100,13 @@ function validateScript(
|
|||||||
script: ScriptData,
|
script: ScriptData,
|
||||||
validator: ExecutableValidator,
|
validator: ExecutableValidator,
|
||||||
): asserts script is NonNullable<ScriptData> {
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator.assertDefined(script);
|
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
||||||
|
value: script,
|
||||||
|
valueName: script.name ?? 'script',
|
||||||
|
allowedProperties: [
|
||||||
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
|
],
|
||||||
|
}));
|
||||||
validator.assertValidName(script.name);
|
validator.assertValidName(script.name);
|
||||||
validator.assert(
|
validator.assert(
|
||||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
@@ -112,7 +123,7 @@ function validateScript(
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface ScriptParserUtilities {
|
interface ScriptParserUtilities {
|
||||||
readonly levelParser: IEnumParser<RecommendationLevel>;
|
readonly levelParser: EnumParser<RecommendationLevel>;
|
||||||
readonly createScript: ScriptFactory;
|
readonly createScript: ScriptFactory;
|
||||||
readonly codeValidator: ICodeValidator;
|
readonly codeValidator: ICodeValidator;
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
@@ -129,7 +140,7 @@ const createScript: ScriptFactory = (...parameters) => {
|
|||||||
return new CollectionScript(...parameters);
|
return new CollectionScript(...parameters);
|
||||||
};
|
};
|
||||||
|
|
||||||
const DefaultScriptParserUtilities: ScriptParserUtilities = {
|
const DefaultUtilities: ScriptParserUtilities = {
|
||||||
levelParser: createEnumParser(RecommendationLevel),
|
levelParser: createEnumParser(RecommendationLevel),
|
||||||
createScript,
|
createScript,
|
||||||
codeValidator: CodeValidator.instance,
|
codeValidator: CodeValidator.instance,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
import { isString } from '@/TypeHelpers';
|
||||||
import type { ExecutableData } from '@/application/collections/';
|
import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator';
|
||||||
import { type ExecutableErrorContext } from './ExecutableErrorContext';
|
import { type ExecutableErrorContext } from './ExecutableErrorContext';
|
||||||
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
|
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
|
||||||
|
|
||||||
@@ -7,11 +7,11 @@ export interface ExecutableValidatorFactory {
|
|||||||
(context: ExecutableErrorContext): ExecutableValidator;
|
(context: ExecutableErrorContext): ExecutableValidator;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AssertTypeFunction = (validator: TypeValidator) => void;
|
||||||
|
|
||||||
export interface ExecutableValidator {
|
export interface ExecutableValidator {
|
||||||
assertValidName(nameValue: string): void;
|
assertValidName(nameValue: string): void;
|
||||||
assertDefined(
|
assertType(assert: AssertTypeFunction): void;
|
||||||
data: ExecutableData | undefined,
|
|
||||||
): asserts data is NonNullable<ExecutableData> & void;
|
|
||||||
assert(
|
assert(
|
||||||
validationPredicate: () => boolean,
|
validationPredicate: () => boolean,
|
||||||
errorMessage: string,
|
errorMessage: string,
|
||||||
@@ -27,6 +27,7 @@ export class ContextualExecutableValidator implements ExecutableValidator {
|
|||||||
private readonly context: ExecutableErrorContext,
|
private readonly context: ExecutableErrorContext,
|
||||||
private readonly createErrorMessage
|
private readonly createErrorMessage
|
||||||
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
|
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
|
||||||
|
private readonly validator: TypeValidator = createTypeValidator(),
|
||||||
) {
|
) {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -39,13 +40,12 @@ export class ContextualExecutableValidator implements ExecutableValidator {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public assertDefined(
|
public assertType(assert: AssertTypeFunction): void {
|
||||||
data: ExecutableData,
|
try {
|
||||||
): asserts data is NonNullable<ExecutableData> {
|
assert(this.validator);
|
||||||
this.assert(
|
} catch (error) {
|
||||||
() => data !== undefined && data !== null && Object.keys(data).length > 0,
|
this.throw(error.message);
|
||||||
'missing executable data',
|
}
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public assert(
|
public assert(
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ parseProjectDetails(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ProjectDetailsParser {
|
||||||
|
(): ProjectDetails;
|
||||||
|
}
|
||||||
|
|
||||||
export type ProjectDetailsFactory = (
|
export type ProjectDetailsFactory = (
|
||||||
...args: ConstructorArguments<typeof GitHubProjectDetails>
|
...args: ConstructorArguments<typeof GitHubProjectDetails>
|
||||||
) => ProjectDetails;
|
) => ProjectDetails;
|
||||||
|
|||||||
@@ -3,28 +3,54 @@ import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|||||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser } from '../../Common/Enum';
|
import { createEnumParser, type EnumParser } from '../../Common/Enum';
|
||||||
|
import { createTypeValidator, type TypeValidator } from '../Common/TypeValidator';
|
||||||
import { CodeSubstituter } from './CodeSubstituter';
|
import { CodeSubstituter } from './CodeSubstituter';
|
||||||
import type { ICodeSubstituter } from './ICodeSubstituter';
|
import type { ICodeSubstituter } from './ICodeSubstituter';
|
||||||
|
|
||||||
export class ScriptingDefinitionParser {
|
export const parseScriptingDefinition: ScriptingDefinitionParser = (
|
||||||
constructor(
|
definition,
|
||||||
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
projectDetails,
|
||||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
utilities: ScriptingDefinitionParserUtilities = DefaultUtilities,
|
||||||
) {
|
) => {
|
||||||
}
|
validateData(definition, utilities.validator);
|
||||||
|
const language = utilities.languageParser.parseEnum(definition.language, 'language');
|
||||||
public parse(
|
const startCode = utilities.codeSubstituter.substitute(definition.startCode, projectDetails);
|
||||||
definition: ScriptingDefinitionData,
|
const endCode = utilities.codeSubstituter.substitute(definition.endCode, projectDetails);
|
||||||
projectDetails: ProjectDetails,
|
|
||||||
): IScriptingDefinition {
|
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
|
||||||
const startCode = this.codeSubstituter.substitute(definition.startCode, projectDetails);
|
|
||||||
const endCode = this.codeSubstituter.substitute(definition.endCode, projectDetails);
|
|
||||||
return new ScriptingDefinition(
|
return new ScriptingDefinition(
|
||||||
language,
|
language,
|
||||||
startCode,
|
startCode,
|
||||||
endCode,
|
endCode,
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ScriptingDefinitionParser {
|
||||||
|
(
|
||||||
|
definition: ScriptingDefinitionData,
|
||||||
|
projectDetails: ProjectDetails,
|
||||||
|
utilities?: ScriptingDefinitionParserUtilities,
|
||||||
|
): IScriptingDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function validateData(
|
||||||
|
data: ScriptingDefinitionData,
|
||||||
|
validator: TypeValidator,
|
||||||
|
): void {
|
||||||
|
validator.assertObject({
|
||||||
|
value: data,
|
||||||
|
valueName: 'scripting definition',
|
||||||
|
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ScriptingDefinitionParserUtilities {
|
||||||
|
readonly languageParser: EnumParser<ScriptingLanguage>;
|
||||||
|
readonly codeSubstituter: ICodeSubstituter;
|
||||||
|
readonly validator: TypeValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultUtilities: ScriptingDefinitionParserUtilities = {
|
||||||
|
languageParser: createEnumParser(ScriptingLanguage),
|
||||||
|
codeSubstituter: new CodeSubstituter(),
|
||||||
|
validator: createTypeValidator(),
|
||||||
|
};
|
||||||
|
|||||||
@@ -8,19 +8,27 @@ import type { IScriptingDefinition } from './IScriptingDefinition';
|
|||||||
import type { ICategoryCollection } from './ICategoryCollection';
|
import type { ICategoryCollection } from './ICategoryCollection';
|
||||||
|
|
||||||
export class CategoryCollection implements ICategoryCollection {
|
export class CategoryCollection implements ICategoryCollection {
|
||||||
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
|
public readonly actions: ReadonlyArray<Category>;
|
||||||
|
|
||||||
|
public readonly scripting: IScriptingDefinition;
|
||||||
|
|
||||||
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
public get totalScripts(): number { return this.queryable.allScripts.length; }
|
||||||
|
|
||||||
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
public get totalCategories(): number { return this.queryable.allCategories.length; }
|
||||||
|
|
||||||
private readonly queryable: IQueryableCollection;
|
private readonly queryable: QueryableCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly os: OperatingSystem,
|
parameters: CategoryCollectionInitParameters,
|
||||||
public readonly actions: ReadonlyArray<Category>,
|
|
||||||
public readonly scripting: IScriptingDefinition,
|
|
||||||
) {
|
) {
|
||||||
this.queryable = makeQueryable(actions);
|
this.os = parameters.os;
|
||||||
assertInRange(os, OperatingSystem);
|
this.actions = parameters.actions;
|
||||||
|
this.scripting = parameters.scripting;
|
||||||
|
|
||||||
|
this.queryable = makeQueryable(this.actions);
|
||||||
|
assertInRange(this.os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
ensureNoDuplicates(this.queryable.allCategories);
|
ensureNoDuplicates(this.queryable.allCategories);
|
||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
@@ -72,13 +80,19 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IQueryableCollection {
|
export interface CategoryCollectionInitParameters {
|
||||||
allCategories: Category[];
|
readonly os: OperatingSystem;
|
||||||
allScripts: Script[];
|
readonly actions: ReadonlyArray<Category>;
|
||||||
scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
|
readonly scripting: IScriptingDefinition;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValid(application: IQueryableCollection) {
|
interface QueryableCollection {
|
||||||
|
readonly allCategories: Category[];
|
||||||
|
readonly allScripts: Script[];
|
||||||
|
readonly scriptsByLevel: Map<RecommendationLevel, readonly Script[]>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValid(application: QueryableCollection) {
|
||||||
ensureValidCategories(application.allCategories);
|
ensureValidCategories(application.allCategories);
|
||||||
ensureValidScripts(application.allScripts);
|
ensureValidScripts(application.allScripts);
|
||||||
}
|
}
|
||||||
@@ -128,7 +142,7 @@ function flattenApplication(
|
|||||||
|
|
||||||
function makeQueryable(
|
function makeQueryable(
|
||||||
actions: ReadonlyArray<Category>,
|
actions: ReadonlyArray<Category>,
|
||||||
): IQueryableCollection {
|
): QueryableCollection {
|
||||||
const flattened = flattenApplication(actions);
|
const flattened = flattenApplication(actions);
|
||||||
return {
|
return {
|
||||||
allCategories: flattened[0],
|
allCategories: flattened[0],
|
||||||
|
|||||||
47
tests/shared/Assertions/ExpectDeepIncludes.ts
Normal file
47
tests/shared/Assertions/ExpectDeepIncludes.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that an array deeply includes a specified item by comparing JSON-serialized versions.
|
||||||
|
* Designed to be used as the Chai methods 'to.deep.include' and 'to.deep.contain' do not work.
|
||||||
|
*/
|
||||||
|
export function expectDeepIncludes<T>(
|
||||||
|
arrayToSearch: readonly T[],
|
||||||
|
expectedItem: T,
|
||||||
|
) {
|
||||||
|
const serializedItemsFromArray = arrayToSearch.map((c) => jsonSerializeForComparison(c));
|
||||||
|
const serializedExpectedItem = jsonSerializeForComparison(expectedItem);
|
||||||
|
expect(serializedItemsFromArray).to.include(serializedExpectedItem, formatAssertionMessage([
|
||||||
|
'Mismatch in expected items.',
|
||||||
|
'The provided array does not include the expected item.',
|
||||||
|
'Expected item:',
|
||||||
|
indentText(serializeItemForDisplay(expectedItem)),
|
||||||
|
`Provided items (total: ${arrayToSearch.length}):`,
|
||||||
|
indentText(serializeArrayForDisplay(arrayToSearch)),
|
||||||
|
]));
|
||||||
|
}
|
||||||
|
|
||||||
|
function jsonSerializeForComparison(obj: unknown): string {
|
||||||
|
return JSON.stringify(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeArrayForDisplay<T>(array: readonly T[]): string {
|
||||||
|
return array.map((item) => indentText(serializeItemForDisplay(item))).join('\n-\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
function serializeItemForDisplay(item: unknown): string {
|
||||||
|
const typeDescription = getTypeDescription(item);
|
||||||
|
const jsonSerializedItem = JSON.stringify(item, null, 2);
|
||||||
|
return `${typeDescription}\n${jsonSerializedItem}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getTypeDescription(item: unknown): string {
|
||||||
|
// Basic type detection using typeof
|
||||||
|
let type = typeof item;
|
||||||
|
// More specific type detection for object types using Object.prototype.toString
|
||||||
|
if (type === 'object') {
|
||||||
|
const preciseType = Object.prototype.toString.call(item);
|
||||||
|
type = preciseType.replace(/^\[object (\S+)\]$/, '$1');
|
||||||
|
}
|
||||||
|
return `Type: ${type}`;
|
||||||
|
}
|
||||||
@@ -1,20 +1,20 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
||||||
import { type CategoryCollectionParserType, parseApplication } from '@/application/Parser/ApplicationParser';
|
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||||
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
import { CategoryCollectionStub } from '@tests/unit/shared/Stubs/CategoryCollectionStub';
|
||||||
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
import { CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
import { AppMetadataStub } from '@tests/unit/shared/Stubs/AppMetadataStub';
|
|
||||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
|
||||||
import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub';
|
import { CategoryCollectionParserStub } from '@tests/unit/shared/Stubs/CategoryCollectionParserStub';
|
||||||
import { ProjectDetailsParserStub } from '@tests/unit/shared/Stubs/ProjectDetailsParserStub';
|
import { ProjectDetailsParserStub } from '@tests/unit/shared/Stubs/ProjectDetailsParserStub';
|
||||||
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
||||||
|
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
|
||||||
|
import type { NonEmptyCollectionAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
|
||||||
describe('ApplicationParser', () => {
|
describe('ApplicationParser', () => {
|
||||||
describe('parseApplication', () => {
|
describe('parseApplication', () => {
|
||||||
@@ -71,45 +71,21 @@ describe('ApplicationParser', () => {
|
|||||||
)).to.equal(true);
|
)).to.equal(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('metadata', () => {
|
|
||||||
it('used to parse expected metadata', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedMetadata = new AppMetadataStub();
|
|
||||||
const projectDetailsParser = new ProjectDetailsParserStub();
|
|
||||||
// act
|
|
||||||
new ApplicationParserBuilder()
|
|
||||||
.withMetadata(expectedMetadata)
|
|
||||||
.withProjectDetailsParser(projectDetailsParser.getStub())
|
|
||||||
.parseApplication();
|
|
||||||
// assert
|
|
||||||
expect(projectDetailsParser.arguments).to.have.lengthOf(1);
|
|
||||||
expect(projectDetailsParser.arguments[0]).to.equal(expectedMetadata);
|
|
||||||
});
|
|
||||||
it('defaults to metadata from factory', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedMetadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance;
|
|
||||||
const projectDetailsParser = new ProjectDetailsParserStub();
|
|
||||||
// act
|
|
||||||
new ApplicationParserBuilder()
|
|
||||||
.withMetadata(undefined) // force using default
|
|
||||||
.withProjectDetailsParser(projectDetailsParser.getStub())
|
|
||||||
.parseApplication();
|
|
||||||
// assert
|
|
||||||
expect(projectDetailsParser.arguments).to.have.lengthOf(1);
|
|
||||||
expect(projectDetailsParser.arguments[0]).to.equal(expectedMetadata);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
describe('collectionsData', () => {
|
describe('collectionsData', () => {
|
||||||
describe('set as expected', () => {
|
describe('set as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases = [
|
const testScenarios: {
|
||||||
|
readonly description: string;
|
||||||
|
readonly input: readonly CollectionData[];
|
||||||
|
readonly output: readonly ICategoryCollection[];
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
name: 'single collection',
|
description: 'single collection',
|
||||||
input: [new CollectionDataStub()],
|
input: [new CollectionDataStub()],
|
||||||
output: [new CategoryCollectionStub().withOs(OperatingSystem.macOS)],
|
output: [new CategoryCollectionStub().withOs(OperatingSystem.macOS)],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'multiple collections',
|
description: 'multiple collections',
|
||||||
input: [
|
input: [
|
||||||
new CollectionDataStub().withOs('windows'),
|
new CollectionDataStub().withOs('windows'),
|
||||||
new CollectionDataStub().withOs('macos'),
|
new CollectionDataStub().withOs('macos'),
|
||||||
@@ -121,22 +97,24 @@ describe('ApplicationParser', () => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
// act
|
// act
|
||||||
for (const testCase of testCases) {
|
testScenarios.forEach(({
|
||||||
it(testCase.name, () => {
|
description, input, output,
|
||||||
|
}) => {
|
||||||
|
it(description, () => {
|
||||||
let categoryParserStub = new CategoryCollectionParserStub();
|
let categoryParserStub = new CategoryCollectionParserStub();
|
||||||
for (let i = 0; i < testCase.input.length; i++) {
|
for (let i = 0; i < input.length; i++) {
|
||||||
categoryParserStub = categoryParserStub
|
categoryParserStub = categoryParserStub
|
||||||
.withReturnValue(testCase.input[i], testCase.output[i]);
|
.withReturnValue(input[i], output[i]);
|
||||||
}
|
}
|
||||||
const sut = new ApplicationParserBuilder()
|
const sut = new ApplicationParserBuilder()
|
||||||
.withCategoryCollectionParser(categoryParserStub.getStub())
|
.withCategoryCollectionParser(categoryParserStub.getStub())
|
||||||
.withCollectionsData(testCase.input);
|
.withCollectionsData(input);
|
||||||
// act
|
// act
|
||||||
const app = sut.parseApplication();
|
const app = sut.parseApplication();
|
||||||
// assert
|
// assert
|
||||||
expect(app.collections).to.deep.equal(testCase.output);
|
expect(app.collections).to.deep.equal(output);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
it('defaults to expected data', () => {
|
it('defaults to expected data', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -151,30 +129,21 @@ describe('ApplicationParser', () => {
|
|||||||
const actual = categoryParserStub.arguments.map((args) => args.data);
|
const actual = categoryParserStub.arguments.map((args) => args.data);
|
||||||
expect(actual).to.deep.equal(expected);
|
expect(actual).to.deep.equal(expected);
|
||||||
});
|
});
|
||||||
describe('throws when data is invalid', () => {
|
it('validates non empty array', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testCases = [
|
const data = [new CollectionDataStub()];
|
||||||
...getAbsentCollectionTestCases<CollectionData>(
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
{
|
value: data,
|
||||||
excludeUndefined: true,
|
valueName: 'collections',
|
||||||
excludeNull: true,
|
};
|
||||||
},
|
const validator = new TypeValidatorStub();
|
||||||
).map((testCase) => ({
|
|
||||||
name: `given absent collection "${testCase.valueName}"`,
|
|
||||||
value: testCase.absentValue,
|
|
||||||
expectedError: 'missing collections',
|
|
||||||
})),
|
|
||||||
];
|
|
||||||
for (const testCase of testCases) {
|
|
||||||
it(testCase.name, () => {
|
|
||||||
const sut = new ApplicationParserBuilder()
|
const sut = new ApplicationParserBuilder()
|
||||||
.withCollectionsData(testCase.value);
|
.withCollectionsData(data)
|
||||||
|
.withTypeValidator(validator);
|
||||||
// act
|
// act
|
||||||
const act = () => sut.parseApplication();
|
sut.parseApplication();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(testCase.expectedError);
|
validator.expectNonEmptyCollectionAssertion(expectedAssertion);
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -182,17 +151,17 @@ describe('ApplicationParser', () => {
|
|||||||
|
|
||||||
class ApplicationParserBuilder {
|
class ApplicationParserBuilder {
|
||||||
private categoryCollectionParser
|
private categoryCollectionParser
|
||||||
: CategoryCollectionParserType = new CategoryCollectionParserStub().getStub();
|
: CategoryCollectionParser = new CategoryCollectionParserStub().getStub();
|
||||||
|
|
||||||
private projectDetailsParser
|
private projectDetailsParser
|
||||||
: typeof parseProjectDetails = new ProjectDetailsParserStub().getStub();
|
: typeof parseProjectDetails = new ProjectDetailsParserStub().getStub();
|
||||||
|
|
||||||
private metadata: IAppMetadata | undefined = new AppMetadataStub();
|
private validator: TypeValidator = new TypeValidatorStub();
|
||||||
|
|
||||||
private collectionsData: CollectionData[] | undefined = [new CollectionDataStub()];
|
private collectionsData: readonly CollectionData[] | undefined = [new CollectionDataStub()];
|
||||||
|
|
||||||
public withCategoryCollectionParser(
|
public withCategoryCollectionParser(
|
||||||
categoryCollectionParser: CategoryCollectionParserType,
|
categoryCollectionParser: CategoryCollectionParser,
|
||||||
): this {
|
): this {
|
||||||
this.categoryCollectionParser = categoryCollectionParser;
|
this.categoryCollectionParser = categoryCollectionParser;
|
||||||
return this;
|
return this;
|
||||||
@@ -205,24 +174,24 @@ class ApplicationParserBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withMetadata(
|
public withCollectionsData(collectionsData: readonly CollectionData[] | undefined): this {
|
||||||
metadata: IAppMetadata | undefined,
|
this.collectionsData = collectionsData;
|
||||||
): this {
|
|
||||||
this.metadata = metadata;
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withCollectionsData(collectionsData: CollectionData[] | undefined): this {
|
public withTypeValidator(validator: TypeValidator): this {
|
||||||
this.collectionsData = collectionsData;
|
this.validator = validator;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public parseApplication(): ReturnType<typeof parseApplication> {
|
public parseApplication(): ReturnType<typeof parseApplication> {
|
||||||
return parseApplication(
|
return parseApplication(
|
||||||
this.categoryCollectionParser,
|
|
||||||
this.projectDetailsParser,
|
|
||||||
this.metadata,
|
|
||||||
this.collectionsData,
|
this.collectionsData,
|
||||||
|
{
|
||||||
|
parseCategoryCollection: this.categoryCollectionParser,
|
||||||
|
parseProjectDetails: this.projectDetailsParser,
|
||||||
|
validator: this.validator,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,119 +1,313 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser';
|
import { parseCategoryCollection, type CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||||
import { parseCategory } from '@/application/Parser/Executable/CategoryParser';
|
import { type CategoryParser } from '@/application/Parser/Executable/CategoryParser';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
|
||||||
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
|
||||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||||
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
||||||
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
import { getCategoryStub, CollectionDataStub } from '@tests/unit/shared/Stubs/CollectionDataStub';
|
||||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
|
||||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
|
||||||
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
import { createFunctionDataWithCode } from '@tests/unit/shared/Stubs/FunctionDataStub';
|
||||||
import { FunctionCallDataStub } from '@tests/unit/shared/Stubs/FunctionCallDataStub';
|
import type { CollectionData, ScriptingDefinitionData, FunctionData } from '@/application/collections/';
|
||||||
import { itEachAbsentCollectionValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import type { CategoryData } from '@/application/collections/';
|
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import type { EnumParser } from '@/application/Common/Enum';
|
||||||
|
import type { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
import { ScriptingDefinitionStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionStub';
|
||||||
|
import type { CategoryCollectionSpecificUtilitiesFactory } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||||
|
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
|
||||||
|
import { CategoryParserStub } from '@tests/unit/shared/Stubs/CategoryParserStub';
|
||||||
|
import { createCategoryCollectionFactorySpy } from '@tests/unit/shared/Stubs/CategoryCollectionFactoryStub';
|
||||||
|
import { CategoryStub } from '@tests/unit/shared/Stubs/CategoryStub';
|
||||||
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
|
||||||
describe('CategoryCollectionParser', () => {
|
describe('CategoryCollectionParser', () => {
|
||||||
describe('parseCategoryCollection', () => {
|
describe('parseCategoryCollection', () => {
|
||||||
describe('actions', () => {
|
it('validates object', () => {
|
||||||
describe('throws with absent actions', () => {
|
|
||||||
itEachAbsentCollectionValue<CategoryData>((absentValue) => {
|
|
||||||
// arrange
|
// arrange
|
||||||
const expectedError = 'content does not define any action';
|
const data = new CollectionDataStub();
|
||||||
const collection = new CollectionDataStub()
|
const expectedAssertion: ObjectAssertion<CollectionData> = {
|
||||||
.withActions(absentValue);
|
value: data,
|
||||||
const projectDetails = new ProjectDetailsStub();
|
valueName: 'collection',
|
||||||
|
allowedProperties: [
|
||||||
|
'os', 'scripting', 'actions', 'functions',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const validator = new TypeValidatorStub();
|
||||||
|
const context = new TestContext()
|
||||||
|
.withData(data)
|
||||||
|
.withTypeValidator(validator);
|
||||||
// act
|
// act
|
||||||
const act = () => parseCategoryCollection(collection, projectDetails);
|
context.parseCategoryCollection();
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.throw(expectedError);
|
validator.expectObjectAssertion(expectedAssertion);
|
||||||
}, { excludeUndefined: true, excludeNull: true });
|
|
||||||
});
|
});
|
||||||
it('parses actions', () => {
|
describe('actions', () => {
|
||||||
|
it('validates non-empty collection', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
const actions = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||||
const context = new CategoryCollectionSpecificUtilitiesStub();
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
const expected = [parseCategory(actions[0], context), parseCategory(actions[1], context)];
|
value: actions,
|
||||||
const collection = new CollectionDataStub()
|
valueName: '"actions" in collection',
|
||||||
.withActions(actions);
|
};
|
||||||
const projectDetails = new ProjectDetailsStub();
|
const validator = new TypeValidatorStub();
|
||||||
|
const context = new TestContext()
|
||||||
|
.withData(new CollectionDataStub().withActions(actions))
|
||||||
|
.withTypeValidator(validator);
|
||||||
// act
|
// act
|
||||||
const actual = parseCategoryCollection(collection, projectDetails).actions;
|
context.parseCategoryCollection();
|
||||||
// assert
|
// assert
|
||||||
expect(excludingId(actual)).to.be.deep.equal(excludingId(expected));
|
validator.expectNonEmptyCollectionAssertion(expectedAssertion);
|
||||||
function excludingId<TId>(array: ReadonlyArray<IEntity<TId>>) {
|
});
|
||||||
return array.map((obj) => {
|
it('parses actions correctly', () => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// arrange
|
||||||
const { id: omitted, ...rest } = obj;
|
const {
|
||||||
return rest;
|
categoryCollectionFactorySpy,
|
||||||
|
getInitParameters,
|
||||||
|
} = createCategoryCollectionFactorySpy();
|
||||||
|
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||||
|
const expectedActions = [new CategoryStub(1), new CategoryStub(2)];
|
||||||
|
const categoryParserStub = new CategoryParserStub()
|
||||||
|
.withConfiguredParseResult(actionsData[0], expectedActions[0])
|
||||||
|
.withConfiguredParseResult(actionsData[1], expectedActions[1]);
|
||||||
|
const collectionData = new CollectionDataStub()
|
||||||
|
.withActions(actionsData);
|
||||||
|
const context = new TestContext()
|
||||||
|
.withData(collectionData)
|
||||||
|
.withCategoryParser(categoryParserStub.get())
|
||||||
|
.withCategoryCollectionFactory(categoryCollectionFactorySpy);
|
||||||
|
// act
|
||||||
|
const actualCollection = context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
const actualActions = getInitParameters(actualCollection)?.actions;
|
||||||
|
expect(actualActions).to.have.lengthOf(expectedActions.length);
|
||||||
|
expect(actualActions).to.have.members(expectedActions);
|
||||||
|
});
|
||||||
|
describe('utilities', () => {
|
||||||
|
it('parses actions with correct utilities', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedUtilities = new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = () => {
|
||||||
|
return expectedUtilities;
|
||||||
|
};
|
||||||
|
const actionsData = [getCategoryStub('test1'), getCategoryStub('test2')];
|
||||||
|
const collectionData = new CollectionDataStub()
|
||||||
|
.withActions(actionsData);
|
||||||
|
const categoryParserStub = new CategoryParserStub();
|
||||||
|
const context = new TestContext()
|
||||||
|
.withData(collectionData)
|
||||||
|
.withCollectionUtilitiesFactory(utilitiesFactory)
|
||||||
|
.withCategoryParser(categoryParserStub.get());
|
||||||
|
// act
|
||||||
|
context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
const usedUtilities = categoryParserStub.getUsedUtilities();
|
||||||
|
expect(usedUtilities).to.have.lengthOf(2);
|
||||||
|
expect(usedUtilities[0]).to.equal(expectedUtilities);
|
||||||
|
expect(usedUtilities[1]).to.equal(expectedUtilities);
|
||||||
|
});
|
||||||
|
describe('construction', () => {
|
||||||
|
it('creates utilities with correct functions data', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedFunctionsData = [createFunctionDataWithCode()];
|
||||||
|
const collectionData = new CollectionDataStub()
|
||||||
|
.withFunctions(expectedFunctionsData);
|
||||||
|
let actualFunctionsData: ReadonlyArray<FunctionData> | undefined;
|
||||||
|
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (data) => {
|
||||||
|
actualFunctionsData = data;
|
||||||
|
return new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
};
|
||||||
|
const context = new TestContext()
|
||||||
|
.withData(collectionData)
|
||||||
|
.withCollectionUtilitiesFactory(utilitiesFactory);
|
||||||
|
// act
|
||||||
|
context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
expect(actualFunctionsData).to.equal(expectedFunctionsData);
|
||||||
|
});
|
||||||
|
it('creates utilities with correct scripting definition', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedScripting = new ScriptingDefinitionStub();
|
||||||
|
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
|
||||||
|
return expectedScripting;
|
||||||
|
};
|
||||||
|
let actualScripting: IScriptingDefinition | undefined;
|
||||||
|
const utilitiesFactory: CategoryCollectionSpecificUtilitiesFactory = (_, scripting) => {
|
||||||
|
actualScripting = scripting;
|
||||||
|
return new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
};
|
||||||
|
const context = new TestContext()
|
||||||
|
.withCollectionUtilitiesFactory(utilitiesFactory)
|
||||||
|
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||||
|
// act
|
||||||
|
context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
expect(actualScripting).to.equal(expectedScripting);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('scripting definition', () => {
|
describe('scripting definition', () => {
|
||||||
it('parses scripting definition as expected', () => {
|
it('parses correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const collection = new CollectionDataStub();
|
const {
|
||||||
const projectDetails = new ProjectDetailsStub();
|
categoryCollectionFactorySpy,
|
||||||
const expected = new ScriptingDefinitionParser()
|
getInitParameters,
|
||||||
.parse(collection.scripting, projectDetails);
|
} = createCategoryCollectionFactorySpy();
|
||||||
|
const expected = new ScriptingDefinitionStub();
|
||||||
|
const scriptingDefinitionParser: ScriptingDefinitionParser = () => {
|
||||||
|
return expected;
|
||||||
|
};
|
||||||
|
const context = new TestContext()
|
||||||
|
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
|
||||||
|
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||||
// act
|
// act
|
||||||
const actual = parseCategoryCollection(collection, projectDetails).scripting;
|
const actualCategoryCollection = context.parseCategoryCollection();
|
||||||
// assert
|
// assert
|
||||||
expect(expected).to.deep.equal(actual);
|
const actualScripting = getInitParameters(actualCategoryCollection)?.scripting;
|
||||||
|
expect(expected).to.equal(actualScripting);
|
||||||
|
});
|
||||||
|
it('parses expected data', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedData = new ScriptingDefinitionDataStub();
|
||||||
|
const collection = new CollectionDataStub()
|
||||||
|
.withScripting(expectedData);
|
||||||
|
let actualData: ScriptingDefinitionData | undefined;
|
||||||
|
const scriptingDefinitionParser
|
||||||
|
: ScriptingDefinitionParser = (data: ScriptingDefinitionData) => {
|
||||||
|
actualData = data;
|
||||||
|
return new ScriptingDefinitionStub();
|
||||||
|
};
|
||||||
|
const context = new TestContext()
|
||||||
|
.withScriptDefinitionParser(scriptingDefinitionParser)
|
||||||
|
.withData(collection);
|
||||||
|
// act
|
||||||
|
context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
expect(actualData).to.equal(expectedData);
|
||||||
|
});
|
||||||
|
it('parses with correct project details', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedProjectDetails = new ProjectDetailsStub();
|
||||||
|
let actualDetails: ProjectDetails | undefined;
|
||||||
|
const scriptingDefinitionParser
|
||||||
|
: ScriptingDefinitionParser = (_, details: ProjectDetails) => {
|
||||||
|
actualDetails = details;
|
||||||
|
return new ScriptingDefinitionStub();
|
||||||
|
};
|
||||||
|
const context = new TestContext()
|
||||||
|
.withProjectDetails(expectedProjectDetails)
|
||||||
|
.withScriptDefinitionParser(scriptingDefinitionParser);
|
||||||
|
// act
|
||||||
|
context.parseCategoryCollection();
|
||||||
|
// assert
|
||||||
|
expect(actualDetails).to.equal(expectedProjectDetails);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('os', () => {
|
describe('os', () => {
|
||||||
it('parses as expected', () => {
|
it('parses correctly', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const {
|
||||||
|
categoryCollectionFactorySpy,
|
||||||
|
getInitParameters,
|
||||||
|
} = createCategoryCollectionFactorySpy();
|
||||||
const expectedOs = OperatingSystem.macOS;
|
const expectedOs = OperatingSystem.macOS;
|
||||||
const osText = 'macos';
|
const osText = 'macos';
|
||||||
const expectedName = 'os';
|
const expectedName = 'os';
|
||||||
const collection = new CollectionDataStub()
|
const collectionData = new CollectionDataStub()
|
||||||
.withOs(osText);
|
.withOs(osText);
|
||||||
const parserMock = new EnumParserStub<OperatingSystem>()
|
const parserMock = new EnumParserStub<OperatingSystem>()
|
||||||
.setup(expectedName, osText, expectedOs);
|
.setup(expectedName, osText, expectedOs);
|
||||||
const projectDetails = new ProjectDetailsStub();
|
const context = new TestContext()
|
||||||
|
.withOsParser(parserMock)
|
||||||
|
.withCategoryCollectionFactory(categoryCollectionFactorySpy)
|
||||||
|
.withData(collectionData);
|
||||||
// act
|
// act
|
||||||
const actual = parseCategoryCollection(collection, projectDetails, parserMock);
|
const actualCollection = context.parseCategoryCollection();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.os).to.equal(expectedOs);
|
const actualOs = getInitParameters(actualCollection)?.os;
|
||||||
});
|
expect(actualOs).to.equal(expectedOs);
|
||||||
});
|
|
||||||
describe('functions', () => {
|
|
||||||
it('compiles script call with given function', () => {
|
|
||||||
// arrange
|
|
||||||
const expectedCode = 'code-from-the-function';
|
|
||||||
const functionName = 'function-name';
|
|
||||||
const scriptName = 'script-name';
|
|
||||||
const script = createScriptDataWithCall()
|
|
||||||
.withCall(new FunctionCallDataStub().withName(functionName).withParameters({}))
|
|
||||||
.withName(scriptName);
|
|
||||||
const func = createFunctionDataWithCode()
|
|
||||||
.withParametersObject([])
|
|
||||||
.withName(functionName)
|
|
||||||
.withCode(expectedCode);
|
|
||||||
const category = new CategoryDataStub()
|
|
||||||
.withChildren([script,
|
|
||||||
createScriptDataWithCode().withName('2')
|
|
||||||
.withRecommendationLevel(RecommendationLevel.Standard),
|
|
||||||
createScriptDataWithCode()
|
|
||||||
.withName('3').withRecommendationLevel(RecommendationLevel.Strict),
|
|
||||||
]);
|
|
||||||
const collection = new CollectionDataStub()
|
|
||||||
.withActions([category])
|
|
||||||
.withFunctions([func]);
|
|
||||||
const projectDetails = new ProjectDetailsStub();
|
|
||||||
// act
|
|
||||||
const actual = parseCategoryCollection(collection, projectDetails);
|
|
||||||
// assert
|
|
||||||
const actualScript = actual.getScript(scriptName);
|
|
||||||
const actualCode = actualScript.code.execute;
|
|
||||||
expect(actualCode).to.equal(expectedCode);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class TestContext {
|
||||||
|
private data: CollectionData = new CollectionDataStub();
|
||||||
|
|
||||||
|
private projectDetails: ProjectDetails = new ProjectDetailsStub();
|
||||||
|
|
||||||
|
private validator: TypeValidator = new TypeValidatorStub();
|
||||||
|
|
||||||
|
private osParser: EnumParser<OperatingSystem> = new EnumParserStub<OperatingSystem>()
|
||||||
|
.setupDefaultValue(OperatingSystem.Android);
|
||||||
|
|
||||||
|
private collectionUtilitiesFactory
|
||||||
|
: CategoryCollectionSpecificUtilitiesFactory = () => {
|
||||||
|
return new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
};
|
||||||
|
|
||||||
|
private scriptDefinitionParser: ScriptingDefinitionParser = () => new ScriptingDefinitionStub();
|
||||||
|
|
||||||
|
private categoryParser: CategoryParser = new CategoryParserStub().get();
|
||||||
|
|
||||||
|
private categoryCollectionFactory
|
||||||
|
: CategoryCollectionFactory = createCategoryCollectionFactorySpy().categoryCollectionFactorySpy;
|
||||||
|
|
||||||
|
public withData(data: CollectionData): this {
|
||||||
|
this.data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCategoryParser(categoryParser: CategoryParser): this {
|
||||||
|
this.categoryParser = categoryParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCategoryCollectionFactory(categoryCollectionFactory: CategoryCollectionFactory): this {
|
||||||
|
this.categoryCollectionFactory = categoryCollectionFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withProjectDetails(projectDetails: ProjectDetails): this {
|
||||||
|
this.projectDetails = projectDetails;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withOsParser(osParser: EnumParser<OperatingSystem>): this {
|
||||||
|
this.osParser = osParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withScriptDefinitionParser(scriptDefinitionParser: ScriptingDefinitionParser): this {
|
||||||
|
this.scriptDefinitionParser = scriptDefinitionParser;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withTypeValidator(typeValidator: TypeValidator): this {
|
||||||
|
this.validator = typeValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withCollectionUtilitiesFactory(
|
||||||
|
collectionUtilitiesFactory: CategoryCollectionSpecificUtilitiesFactory,
|
||||||
|
): this {
|
||||||
|
this.collectionUtilitiesFactory = collectionUtilitiesFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseCategoryCollection(): ReturnType<typeof parseCategoryCollection> {
|
||||||
|
return parseCategoryCollection(
|
||||||
|
this.data,
|
||||||
|
this.projectDetails,
|
||||||
|
{
|
||||||
|
osParser: this.osParser,
|
||||||
|
validator: this.validator,
|
||||||
|
parseScriptingDefinition: this.scriptDefinitionParser,
|
||||||
|
createUtilities: this.collectionUtilitiesFactory,
|
||||||
|
parseCategory: this.categoryParser,
|
||||||
|
createCategoryCollection: this.categoryCollectionFactory,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { CustomError } from '@/application/Common/CustomError';
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
import { wrapErrorWithAdditionalContext } from '@/application/Parser/ContextualError';
|
import { wrapErrorWithAdditionalContext } from '@/application/Parser/Common/ContextualError';
|
||||||
|
|
||||||
describe('wrapErrorWithAdditionalContext', () => {
|
describe('wrapErrorWithAdditionalContext', () => {
|
||||||
it('preserves the original error when wrapped', () => {
|
it('preserves the original error when wrapped', () => {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
import { indentText } from '@tests/shared/Text';
|
import { indentText } from '@tests/shared/Text';
|
||||||
157
tests/unit/application/Parser/Common/TypeValidator.spec.ts
Normal file
157
tests/unit/application/Parser/Common/TypeValidator.spec.ts
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { createTypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import { itEachAbsentObjectValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
|
||||||
|
describe('createTypeValidator', () => {
|
||||||
|
describe('assertObject', () => {
|
||||||
|
describe('with valid object', () => {
|
||||||
|
it('accepts object with allowed properties', () => {
|
||||||
|
// arrange
|
||||||
|
const expectedProperties = ['expected1', 'expected2'];
|
||||||
|
const validValue = createObjectWithProperties(expectedProperties);
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({
|
||||||
|
value: validValue,
|
||||||
|
valueName: 'unimportant name',
|
||||||
|
allowedProperties: expectedProperties,
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
it('accepts object with extra unspecified properties', () => {
|
||||||
|
// arrange
|
||||||
|
const validValue = createObjectWithProperties(['unevaluated property']);
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({
|
||||||
|
value: validValue,
|
||||||
|
valueName: 'unimportant name',
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with invalid object', () => {
|
||||||
|
describe('throws error for missing object', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const valueName = 'absent object value';
|
||||||
|
const expectedMessage = `'${valueName}' is missing.`;
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({ value: absentValue, valueName });
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws error for object without properties', () => {
|
||||||
|
// arrange
|
||||||
|
const emptyObjectValue: object = {};
|
||||||
|
const valueName = 'empty object without properties.';
|
||||||
|
const expectedMessage = `'${valueName}' is an empty object without properties.`;
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({ value: emptyObjectValue, valueName });
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
describe('incorrect data type', () => {
|
||||||
|
// arrange
|
||||||
|
const testScenarios: readonly {
|
||||||
|
readonly value: unknown;
|
||||||
|
readonly valueName: string;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
value: ['1', '2'],
|
||||||
|
valueName: 'array of strings',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: true,
|
||||||
|
valueName: 'true boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
value: 35,
|
||||||
|
valueName: 'number',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
testScenarios.forEach(({ value, valueName }) => {
|
||||||
|
it(`throws error for ${valueName} passed as object`, () => {
|
||||||
|
// arrange
|
||||||
|
const expectedMessage = `'${valueName}' is not an object.`;
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({ value, valueName });
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws error for object with disallowed properties', () => {
|
||||||
|
// arrange
|
||||||
|
const valueName = 'value with unexpected properties';
|
||||||
|
const unexpectedProperties = ['unexpected-property-1', 'unexpected-property-2'];
|
||||||
|
const expectedError = `'${valueName}' has disallowed properties: ${unexpectedProperties.join(', ')}.`;
|
||||||
|
const expectedProperties = ['expected1', 'expected2'];
|
||||||
|
const value = createObjectWithProperties(
|
||||||
|
[...expectedProperties, ...unexpectedProperties],
|
||||||
|
);
|
||||||
|
const { assertObject } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertObject({
|
||||||
|
value,
|
||||||
|
valueName,
|
||||||
|
allowedProperties: expectedProperties,
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedError);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('assertNonEmptyCollection', () => {
|
||||||
|
describe('with valid collection', () => {
|
||||||
|
it('accepts non-empty collection', () => {
|
||||||
|
// arrange
|
||||||
|
const validValue = ['array', 'of', 'strings'];
|
||||||
|
const { assertNonEmptyCollection } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertNonEmptyCollection({ value: validValue, valueName: 'unimportant name' });
|
||||||
|
// assert
|
||||||
|
expect(act).to.not.throw();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
describe('with invalid collection', () => {
|
||||||
|
describe('throws error for missing collection', () => {
|
||||||
|
itEachAbsentObjectValue((absentValue) => {
|
||||||
|
// arrange
|
||||||
|
const valueName = 'absent collection value';
|
||||||
|
const expectedMessage = `'${valueName}' is missing.`;
|
||||||
|
const { assertNonEmptyCollection } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertNonEmptyCollection({ value: absentValue, valueName });
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('throws error for empty collection', () => {
|
||||||
|
// arrange
|
||||||
|
const emptyArrayValue = [];
|
||||||
|
const valueName = 'empty collection value';
|
||||||
|
const expectedMessage = `'${valueName}' cannot be an empty array.`;
|
||||||
|
const { assertNonEmptyCollection } = createTypeValidator();
|
||||||
|
// act
|
||||||
|
const act = () => assertNonEmptyCollection({ value: emptyArrayValue, valueName });
|
||||||
|
// assert
|
||||||
|
expect(act).to.throw(expectedMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function createObjectWithProperties(properties: readonly string[]): object {
|
||||||
|
const object = {};
|
||||||
|
properties.forEach((propertyName) => {
|
||||||
|
object[propertyName] = 'arbitrary value';
|
||||||
|
});
|
||||||
|
return object;
|
||||||
|
}
|
||||||
@@ -5,10 +5,9 @@ import { type ScriptParser } from '@/application/Parser/Executable/Script/Script
|
|||||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
||||||
import { getAbsentCollectionTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
|
||||||
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
import { ExecutableType } from '@/application/Parser/Executable/Validation/ExecutableType';
|
||||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||||
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
||||||
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
||||||
@@ -20,8 +19,9 @@ import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
|||||||
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
import { ScriptParserStub } from '@tests/unit/shared/Stubs/ScriptParserStub';
|
||||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
import { indentText } from '@tests/shared/Text';
|
import { indentText } from '@tests/shared/Text';
|
||||||
import { itThrowsContextualError } from '../ContextualErrorTester';
|
import type { NonEmptyCollectionAssertion, ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||||
import { itValidatesName, itValidatesDefinedData, itAsserts } from './Validation/ExecutableValidationTester';
|
import { itThrowsContextualError } from '../Common/ContextualErrorTester';
|
||||||
|
import { itValidatesName, itValidatesType, itAsserts } from './Validation/ExecutableValidationTester';
|
||||||
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
import { generateDataValidationTestScenarios } from './Validation/DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('CategoryParser', () => {
|
describe('CategoryParser', () => {
|
||||||
@@ -49,14 +49,18 @@ describe('CategoryParser', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('validates for defined data', () => {
|
describe('validates for unknown object', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const category = new CategoryDataStub();
|
const category = new CategoryDataStub();
|
||||||
const expectedContext: CategoryErrorContext = {
|
const expectedContext: CategoryErrorContext = {
|
||||||
type: ExecutableType.Category,
|
type: ExecutableType.Category,
|
||||||
self: category,
|
self: category,
|
||||||
};
|
};
|
||||||
itValidatesDefinedData(
|
const expectedAssertion: ObjectAssertion<unknown> = {
|
||||||
|
value: category,
|
||||||
|
valueName: 'Executable',
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestBuilder()
|
||||||
@@ -65,58 +69,65 @@ describe('CategoryParser', () => {
|
|||||||
.parseCategory();
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
return {
|
return {
|
||||||
expectedDataToValidate: category,
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
expectedErrorContext: expectedContext,
|
expectedErrorContext: expectedContext,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
describe('validates that category has some children', () => {
|
describe('validates for category', () => {
|
||||||
const categoryName = 'test';
|
// arrange
|
||||||
const testScenarios = generateDataValidationTestScenarios<CategoryData>({
|
const category = new CategoryDataStub();
|
||||||
expectFail: getAbsentCollectionTestCases<ExecutableData>().map(({
|
|
||||||
valueName, absentValue: absentCollectionValue,
|
|
||||||
}) => ({
|
|
||||||
description: `with \`${valueName}\` value as children`,
|
|
||||||
data: new CategoryDataStub()
|
|
||||||
.withName(categoryName)
|
|
||||||
.withChildren(absentCollectionValue as unknown as ExecutableData[]),
|
|
||||||
})),
|
|
||||||
expectPass: [{
|
|
||||||
description: 'has single children',
|
|
||||||
data: new CategoryDataStub()
|
|
||||||
.withName(categoryName)
|
|
||||||
.withChildren([createScriptDataWithCode()]),
|
|
||||||
}],
|
|
||||||
});
|
|
||||||
testScenarios.forEach(({
|
|
||||||
description, expectedPass, data: categoryData,
|
|
||||||
}) => {
|
|
||||||
describe(description, () => {
|
|
||||||
itAsserts({
|
|
||||||
expectedConditionResult: expectedPass,
|
|
||||||
test: (validatorFactory) => {
|
|
||||||
const expectedMessage = `"${categoryName}" has no children.`;
|
|
||||||
const expectedContext: CategoryErrorContext = {
|
const expectedContext: CategoryErrorContext = {
|
||||||
type: ExecutableType.Category,
|
type: ExecutableType.Category,
|
||||||
self: categoryData,
|
self: category,
|
||||||
};
|
};
|
||||||
|
const expectedAssertion: ObjectAssertion<CategoryData> = {
|
||||||
|
value: category,
|
||||||
|
valueName: category.category,
|
||||||
|
allowedProperties: ['docs', 'children', 'category'],
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
try {
|
|
||||||
new TestBuilder()
|
new TestBuilder()
|
||||||
.withData(categoryData)
|
.withData(category)
|
||||||
.withValidatorFactory(validatorFactory)
|
.withValidatorFactory(validatorFactory)
|
||||||
.parseCategory();
|
.parseCategory();
|
||||||
} catch { /* It may throw due to assertions not being evaluated */ }
|
|
||||||
// assert
|
// assert
|
||||||
return {
|
return {
|
||||||
expectedErrorMessage: expectedMessage,
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
expectedErrorContext: expectedContext,
|
expectedErrorContext: expectedContext,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
describe('validates children for non-empty collection', () => {
|
||||||
});
|
// arrange
|
||||||
|
const category = new CategoryDataStub()
|
||||||
|
.withChildren([createScriptDataWithCode()]);
|
||||||
|
const expectedContext: CategoryErrorContext = {
|
||||||
|
type: ExecutableType.Category,
|
||||||
|
self: category,
|
||||||
|
};
|
||||||
|
const expectedAssertion: NonEmptyCollectionAssertion = {
|
||||||
|
value: category.children,
|
||||||
|
valueName: category.category,
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
|
(validatorFactory) => {
|
||||||
|
// act
|
||||||
|
new TestBuilder()
|
||||||
|
.withData(category)
|
||||||
|
.withValidatorFactory(validatorFactory)
|
||||||
|
.parseCategory();
|
||||||
|
// assert
|
||||||
|
return {
|
||||||
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
|
expectedErrorContext: expectedContext,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
describe('validates that a child is a category or a script', () => {
|
describe('validates that a child is a category or a script', () => {
|
||||||
// arrange
|
// arrange
|
||||||
@@ -171,7 +182,7 @@ describe('CategoryParser', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('validates children recursively', () => {
|
describe('validates children recursively', () => {
|
||||||
describe('validates (1th-level) child data', () => {
|
describe('validates (1th-level) child type', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedName = 'child category';
|
const expectedName = 'child category';
|
||||||
const child = new CategoryDataStub()
|
const child = new CategoryDataStub()
|
||||||
@@ -183,7 +194,11 @@ describe('CategoryParser', () => {
|
|||||||
self: child,
|
self: child,
|
||||||
parentCategory: parent,
|
parentCategory: parent,
|
||||||
};
|
};
|
||||||
itValidatesDefinedData(
|
const expectedAssertion: ObjectAssertion<unknown> = {
|
||||||
|
value: child,
|
||||||
|
valueName: 'Executable',
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestBuilder()
|
new TestBuilder()
|
||||||
@@ -192,7 +207,7 @@ describe('CategoryParser', () => {
|
|||||||
.parseCategory();
|
.parseCategory();
|
||||||
// assert
|
// assert
|
||||||
return {
|
return {
|
||||||
expectedDataToValidate: child,
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
expectedErrorContext: expectedContext,
|
expectedErrorContext: expectedContext,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ describe('DocumentationParser', () => {
|
|||||||
});
|
});
|
||||||
describe('throws when type is unexpected', () => {
|
describe('throws when type is unexpected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedTypeError = 'docs field (documentation) must be an array of strings';
|
const expectedTypeError = 'docs field (documentation) must be a single string or an array of strings.';
|
||||||
const wrongTypedValue = 22 as never;
|
const wrongTypedValue = 22 as never;
|
||||||
const testCases: ReadonlyArray<{
|
const testCases: ReadonlyArray<{
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
import { ExpressionPosition } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionPosition';
|
||||||
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { itEachAbsentStringValue } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
import { ExpressionStub } from '@tests/unit/shared/Stubs/ExpressionStub';
|
||||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression';
|
import type { IExpression } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/IExpression';
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import { SingleCallCompilerStub } from '@tests/unit/shared/Stubs/SingleCallCompi
|
|||||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
|
||||||
describe('NestedFunctionCallCompiler', () => {
|
describe('NestedFunctionCallCompiler', () => {
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import { FunctionCallArgumentCollectionStub } from '@tests/unit/shared/Stubs/Fun
|
|||||||
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
import { createSharedFunctionStubWithCode } from '@tests/unit/shared/Stubs/SharedFunctionStub';
|
||||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
import { SharedFunctionCollectionStub } from '@tests/unit/shared/Stubs/SharedFunctionCollectionStub';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
|
|
||||||
describe('NestedFunctionArgumentCompiler', () => {
|
describe('NestedFunctionArgumentCompiler', () => {
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
|||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
import { FunctionParameterStub } from '@tests/unit/shared/Stubs/FunctionParameterStub';
|
||||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
import type { FunctionParameterCollectionFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
import { FunctionParameterCollectionStub } from '@tests/unit/shared/Stubs/FunctionParameterCollectionStub';
|
||||||
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
import { expectCallsFunctionBody, expectCodeFunctionBody } from './ExpectFunctionBodyType';
|
||||||
|
|||||||
@@ -16,12 +16,12 @@ import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
|||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
import { CompiledCodeStub } from '@tests/unit/shared/Stubs/CompiledCodeStub';
|
||||||
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
import { createScriptDataWithCall, createScriptDataWithCode } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
import { errorWithContextWrapperStub } from '@tests/unit/shared/Stubs/ErrorWithContextWrapperStub';
|
||||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||||
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCodeFactoryStub';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
|
|
||||||
describe('ScriptCompiler', () => {
|
describe('ScriptCompiler', () => {
|
||||||
describe('canCompile', () => {
|
describe('canCompile', () => {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData, CallScriptData, CodeScriptData } from '@/application/collections/';
|
||||||
import { parseScript, type ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
|
import { parseScript, type ScriptFactory } from '@/application/Parser/Executable/Script/ScriptParser';
|
||||||
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
import { type DocsParser } from '@/application/Parser/Executable/DocumentationParser';
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
||||||
@@ -8,14 +8,13 @@ import { ScriptCompilerStub } from '@tests/unit/shared/Stubs/ScriptCompilerStub'
|
|||||||
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
import { createScriptDataWithCall, createScriptDataWithCode, createScriptDataWithoutCallOrCodes } from '@tests/unit/shared/Stubs/ScriptDataStub';
|
||||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||||
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
import { ScriptCodeStub } from '@tests/unit/shared/Stubs/ScriptCodeStub';
|
||||||
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
|
||||||
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
import { LanguageSyntaxStub } from '@tests/unit/shared/Stubs/LanguageSyntaxStub';
|
||||||
import type { IEnumParser } from '@/application/Common/Enum';
|
import type { EnumParser } from '@/application/Common/Enum';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
|
||||||
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
import { CodeValidatorStub } from '@tests/unit/shared/Stubs/CodeValidatorStub';
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
import { ErrorWrapperStub } from '@tests/unit/shared/Stubs/ErrorWrapperStub';
|
||||||
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
import type { ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
||||||
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
import { ExecutableValidatorStub, createExecutableValidatorFactoryStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
||||||
@@ -26,9 +25,11 @@ import { createScriptCodeFactoryStub } from '@tests/unit/shared/Stubs/ScriptCode
|
|||||||
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
import { ScriptStub } from '@tests/unit/shared/Stubs/ScriptStub';
|
||||||
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
|
import { createScriptFactorySpy } from '@tests/unit/shared/Stubs/ScriptFactoryStub';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
import { itThrowsContextualError } from '@tests/unit/application/Parser/ContextualErrorTester';
|
import { itThrowsContextualError } from '@tests/unit/application/Parser/Common/ContextualErrorTester';
|
||||||
|
import { CategoryCollectionSpecificUtilitiesStub } from '@tests/unit/shared/Stubs/CategoryCollectionSpecificUtilitiesStub';
|
||||||
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||||
import { itAsserts, itValidatesDefinedData, itValidatesName } from '../Validation/ExecutableValidationTester';
|
import type { ObjectAssertion } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import { itAsserts, itValidatesType, itValidatesName } from '../Validation/ExecutableValidationTester';
|
||||||
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
import { generateDataValidationTestScenarios } from '../Validation/DataValidationTestScenarioGenerator';
|
||||||
|
|
||||||
describe('ScriptParser', () => {
|
describe('ScriptParser', () => {
|
||||||
@@ -290,7 +291,14 @@ describe('ScriptParser', () => {
|
|||||||
type: ExecutableType.Script,
|
type: ExecutableType.Script,
|
||||||
self: expectedScript,
|
self: expectedScript,
|
||||||
};
|
};
|
||||||
itValidatesDefinedData(
|
const expectedAssertion: ObjectAssertion<CallScriptData & CodeScriptData> = {
|
||||||
|
value: expectedScript,
|
||||||
|
valueName: expectedScript.name,
|
||||||
|
allowedProperties: [
|
||||||
|
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
||||||
|
],
|
||||||
|
};
|
||||||
|
itValidatesType(
|
||||||
(validatorFactory) => {
|
(validatorFactory) => {
|
||||||
// act
|
// act
|
||||||
new TestContext()
|
new TestContext()
|
||||||
@@ -301,6 +309,7 @@ describe('ScriptParser', () => {
|
|||||||
return {
|
return {
|
||||||
expectedDataToValidate: expectedScript,
|
expectedDataToValidate: expectedScript,
|
||||||
expectedErrorContext: expectedContext,
|
expectedErrorContext: expectedContext,
|
||||||
|
assertValidation: (validator) => validator.assertObject(expectedAssertion),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@@ -430,7 +439,7 @@ class TestContext {
|
|||||||
private collectionUtilities
|
private collectionUtilities
|
||||||
: CategoryCollectionSpecificUtilities = new CategoryCollectionSpecificUtilitiesStub();
|
: CategoryCollectionSpecificUtilities = new CategoryCollectionSpecificUtilitiesStub();
|
||||||
|
|
||||||
private levelParser: IEnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
private levelParser: EnumParser<RecommendationLevel> = new EnumParserStub<RecommendationLevel>()
|
||||||
.setupDefaultValue(RecommendationLevel.Standard);
|
.setupDefaultValue(RecommendationLevel.Standard);
|
||||||
|
|
||||||
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
|
private scriptFactory: ScriptFactory = createScriptFactorySpy().scriptFactorySpy;
|
||||||
@@ -464,7 +473,7 @@ class TestContext {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withParser(parser: IEnumParser<RecommendationLevel>): this {
|
public withParser(parser: EnumParser<RecommendationLevel>): this {
|
||||||
this.levelParser = parser;
|
this.levelParser = parser;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,10 +3,11 @@ import type { ExecutableValidator, ExecutableValidatorFactory } from '@/applicat
|
|||||||
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
|
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
|
||||||
import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
import { ExecutableValidatorStub } from '@tests/unit/shared/Stubs/ExecutableValidatorStub';
|
||||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||||
import type { ExecutableData } from '@/application/collections/';
|
|
||||||
import type { FunctionKeys } from '@/TypeHelpers';
|
import type { FunctionKeys } from '@/TypeHelpers';
|
||||||
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
import { formatAssertionMessage } from '@tests/shared/FormatAssertionMessage';
|
||||||
import { indentText } from '@tests/shared/Text';
|
import { indentText } from '@tests/shared/Text';
|
||||||
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
|
import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes';
|
||||||
|
|
||||||
type ValidationTestFunction<TExpectation> = (
|
type ValidationTestFunction<TExpectation> = (
|
||||||
factory: ExecutableValidatorFactory,
|
factory: ExecutableValidatorFactory,
|
||||||
@@ -52,39 +53,41 @@ export function itValidatesName(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ValidDataExpectation {
|
interface TypeAssertionExpectation {
|
||||||
readonly expectedDataToValidate: ExecutableData;
|
|
||||||
readonly expectedErrorContext: ExecutableErrorContext;
|
readonly expectedErrorContext: ExecutableErrorContext;
|
||||||
|
readonly assertValidation: (validator: TypeValidatorStub) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function itValidatesDefinedData(
|
export function itValidatesType(
|
||||||
test: ValidationTestFunction<ValidDataExpectation>,
|
test: ValidationTestFunction<TypeAssertionExpectation>,
|
||||||
) {
|
) {
|
||||||
it('validates data', () => {
|
it('validates type', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const validator = new ExecutableValidatorStub();
|
const validator = new ExecutableValidatorStub();
|
||||||
const factoryStub: ExecutableValidatorFactory = () => validator;
|
const factoryStub: ExecutableValidatorFactory = () => validator;
|
||||||
// act
|
// act
|
||||||
test(factoryStub);
|
test(factoryStub);
|
||||||
// assert
|
// assert
|
||||||
const call = validator.callHistory.find((c) => c.methodName === 'assertDefined');
|
const call = validator.callHistory.find((c) => c.methodName === 'assertType');
|
||||||
expectExists(call);
|
expectExists(call);
|
||||||
});
|
});
|
||||||
it('validates data with correct data', () => {
|
it('validates type using specified validator', () => {
|
||||||
// arrange
|
// arrange
|
||||||
|
const typeValidator = new TypeValidatorStub();
|
||||||
const validator = new ExecutableValidatorStub();
|
const validator = new ExecutableValidatorStub();
|
||||||
const factoryStub: ExecutableValidatorFactory = () => validator;
|
const factoryStub: ExecutableValidatorFactory = () => validator;
|
||||||
// act
|
// act
|
||||||
const expectation = test(factoryStub);
|
const expectation = test(factoryStub);
|
||||||
// assert
|
// assert
|
||||||
const expectedData = expectation.expectedDataToValidate;
|
const calls = validator.callHistory.filter((c) => c.methodName === 'assertType');
|
||||||
const calls = validator.callHistory.filter((c) => c.methodName === 'assertDefined');
|
const args = calls.map((c) => c.args as Parameters<ExecutableValidator['assertType']>);
|
||||||
const names = calls.flatMap((c) => c.args[0]);
|
const validateFunctions = args.flatMap((c) => c[0]);
|
||||||
expect(names).to.include(expectedData);
|
validateFunctions.forEach((validate) => validate(typeValidator));
|
||||||
|
expectation.assertValidation(typeValidator);
|
||||||
});
|
});
|
||||||
it('validates data with correct context', () => {
|
it('validates type with correct context', () => {
|
||||||
expectCorrectContextForFunctionCall({
|
expectCorrectContextForFunctionCall({
|
||||||
methodName: 'assertDefined',
|
methodName: 'assertType',
|
||||||
act: test,
|
act: test,
|
||||||
expectContext: (expectation) => expectation.expectedErrorContext,
|
expectContext: (expectation) => expectation.expectedErrorContext,
|
||||||
});
|
});
|
||||||
@@ -185,34 +188,5 @@ function expectCorrectContextForFunctionCall<T>(testScenario: {
|
|||||||
const providedContexts = createdValidators
|
const providedContexts = createdValidators
|
||||||
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
|
.filter((v) => v.validator.callHistory.find((c) => c.methodName === methodName))
|
||||||
.map((v) => v.context);
|
.map((v) => v.context);
|
||||||
expectDeepIncludes( // to.deep.include is not working
|
expectDeepIncludes(providedContexts, expectedContext);
|
||||||
providedContexts,
|
|
||||||
expectedContext,
|
|
||||||
formatAssertionMessage([
|
|
||||||
'Error context mismatch.',
|
|
||||||
'Provided contexts do not include the expected context.',
|
|
||||||
'Expected context:',
|
|
||||||
indentText(JSON.stringify(expectedContext, undefined, 2)),
|
|
||||||
'Provided contexts:',
|
|
||||||
indentText(JSON.stringify(providedContexts, undefined, 2)),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function expectDeepIncludes<T>(
|
|
||||||
array: readonly T[],
|
|
||||||
item: T,
|
|
||||||
message: string,
|
|
||||||
) {
|
|
||||||
const serializeItem = (c) => JSON.stringify(c);
|
|
||||||
const serializedContexts = array.map((c) => serializeItem(c));
|
|
||||||
const serializedExpectedContext = serializeItem(item);
|
|
||||||
expect(serializedContexts).to.include(serializedExpectedContext, formatAssertionMessage([
|
|
||||||
'Error context mismatch.',
|
|
||||||
'Provided contexts do not include the expected context.',
|
|
||||||
'Expected context:',
|
|
||||||
indentText(JSON.stringify(message, undefined, 2)),
|
|
||||||
'Provided contexts:',
|
|
||||||
indentText(JSON.stringify(message, undefined, 2)),
|
|
||||||
]));
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { CategoryDataStub } from '@tests/unit/shared/Stubs/CategoryDataStub';
|
|
||||||
import type { ExecutableData } from '@/application/collections/';
|
|
||||||
import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub';
|
import { createExecutableErrorContextStub } from '@tests/unit/shared/Stubs/ExecutableErrorContextStub';
|
||||||
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
|
import type { ExecutableErrorContext } from '@/application/Parser/Executable/Validation/ExecutableErrorContext';
|
||||||
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
import { collectExceptionMessage } from '@tests/unit/shared/ExceptionCollector';
|
||||||
import { ContextualExecutableValidator, createExecutableDataValidator, type ExecutableValidator } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
import { ContextualExecutableValidator, createExecutableDataValidator, type ExecutableValidator } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
||||||
import type { ExecutableContextErrorMessageCreator } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage';
|
import type { ExecutableContextErrorMessageCreator } from '@/application/Parser/Executable/Validation/ExecutableErrorContextMessage';
|
||||||
import { getAbsentObjectTestCases, getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
import { getAbsentStringTestCases } from '@tests/unit/shared/TestCases/AbsentTests';
|
||||||
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
|
import type { TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
|
||||||
describe('createExecutableDataValidator', () => {
|
describe('createExecutableDataValidator', () => {
|
||||||
it(`returns an instance of ${ContextualExecutableValidator.name}`, () => {
|
it(`returns an instance of ${ContextualExecutableValidator.name}`, () => {
|
||||||
@@ -63,43 +63,41 @@ describe('ContextualExecutableValidator', () => {
|
|||||||
expect(act).to.not.throw();
|
expect(act).to.not.throw();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
describe('assertDefined', () => {
|
describe('assertType', () => {
|
||||||
describe('throws when data is missing', () => {
|
describe('rethrows when action throws', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const testScenarios: readonly {
|
const expectedMessage = 'Error thrown by action';
|
||||||
readonly description: string;
|
|
||||||
readonly invalidData: unknown;
|
|
||||||
}[] = [
|
|
||||||
...getAbsentObjectTestCases().map((testCase) => ({
|
|
||||||
description: `absent object (${testCase.valueName})`,
|
|
||||||
invalidData: testCase.absentValue,
|
|
||||||
})),
|
|
||||||
{
|
|
||||||
description: 'empty object',
|
|
||||||
invalidData: {},
|
|
||||||
},
|
|
||||||
];
|
|
||||||
testScenarios.forEach(({ description, invalidData }) => {
|
|
||||||
describe(`given "${description}"`, () => {
|
|
||||||
const expectedMessage = 'missing executable data';
|
|
||||||
itThrowsCorrectly({
|
itThrowsCorrectly({
|
||||||
// act
|
// act
|
||||||
throwingAction: (sut: ExecutableValidator) => {
|
throwingAction: (sut: ExecutableValidator) => {
|
||||||
sut.assertDefined(invalidData as ExecutableData);
|
sut.assertType(() => {
|
||||||
|
throw new Error(expectedMessage);
|
||||||
|
});
|
||||||
},
|
},
|
||||||
// assert
|
// assert
|
||||||
expectedMessage,
|
expectedMessage,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
it('provides correct validator', () => {
|
||||||
});
|
// arrange
|
||||||
it('does not throw if data is defined', () => {
|
const expectedValidator = new TypeValidatorStub();
|
||||||
|
const sut = new ValidatorBuilder()
|
||||||
|
.withTypeValidator(expectedValidator)
|
||||||
|
.build();
|
||||||
|
let actualValidator: TypeValidator | undefined;
|
||||||
|
// act
|
||||||
|
sut.assertType((validator) => {
|
||||||
|
actualValidator = validator;
|
||||||
|
});
|
||||||
|
// assert
|
||||||
|
expect(expectedValidator).to.equal(actualValidator);
|
||||||
|
});
|
||||||
|
it('does not throw if action does not throw', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const data = new CategoryDataStub();
|
|
||||||
const sut = new ValidatorBuilder()
|
const sut = new ValidatorBuilder()
|
||||||
.build();
|
.build();
|
||||||
// act
|
// act
|
||||||
const act = () => sut.assertDefined(data);
|
const act = () => sut.assertType(() => { /* Does not throw */ });
|
||||||
// assert
|
// assert
|
||||||
expect(act).to.not.throw();
|
expect(act).to.not.throw();
|
||||||
});
|
});
|
||||||
@@ -223,6 +221,8 @@ class ValidatorBuilder {
|
|||||||
|
|
||||||
private errorMessageCreator: ExecutableContextErrorMessageCreator = () => `[${ValidatorBuilder.name}] stub error message`;
|
private errorMessageCreator: ExecutableContextErrorMessageCreator = () => `[${ValidatorBuilder.name}] stub error message`;
|
||||||
|
|
||||||
|
private typeValidator: TypeValidator = new TypeValidatorStub();
|
||||||
|
|
||||||
public withErrorMessageCreator(errorMessageCreator: ExecutableContextErrorMessageCreator): this {
|
public withErrorMessageCreator(errorMessageCreator: ExecutableContextErrorMessageCreator): this {
|
||||||
this.errorMessageCreator = errorMessageCreator;
|
this.errorMessageCreator = errorMessageCreator;
|
||||||
return this;
|
return this;
|
||||||
@@ -233,10 +233,16 @@ class ValidatorBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public withTypeValidator(typeValidator: TypeValidator): this {
|
||||||
|
this.typeValidator = typeValidator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public build(): ContextualExecutableValidator {
|
public build(): ContextualExecutableValidator {
|
||||||
return new ContextualExecutableValidator(
|
return new ContextualExecutableValidator(
|
||||||
this.errorContext,
|
this.errorContext,
|
||||||
this.errorMessageCreator,
|
this.errorMessageCreator,
|
||||||
|
this.typeValidator,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,52 @@
|
|||||||
import { describe, it, expect } from 'vitest';
|
import { describe, it, expect } from 'vitest';
|
||||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||||
import { ScriptingDefinitionParser } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
import type { EnumParser } from '@/application/Common/Enum';
|
||||||
import type { IEnumParser } from '@/application/Common/Enum';
|
|
||||||
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
|
import type { ICodeSubstituter } from '@/application/Parser/ScriptingDefinition/ICodeSubstituter';
|
||||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
import { ProjectDetailsStub } from '@tests/unit/shared/Stubs/ProjectDetailsStub';
|
||||||
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
import { EnumParserStub } from '@tests/unit/shared/Stubs/EnumParserStub';
|
||||||
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
|
import { ScriptingDefinitionDataStub } from '@tests/unit/shared/Stubs/ScriptingDefinitionDataStub';
|
||||||
import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub';
|
import { CodeSubstituterStub } from '@tests/unit/shared/Stubs/CodeSubstituterStub';
|
||||||
|
import { parseScriptingDefinition } from '@/application/Parser/ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
|
import type { ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import { TypeValidatorStub } from '@tests/unit/shared/Stubs/TypeValidatorStub';
|
||||||
|
import type { ScriptingDefinitionData } from '@/application/collections/';
|
||||||
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
|
|
||||||
describe('ScriptingDefinitionParser', () => {
|
describe('ScriptingDefinitionParser', () => {
|
||||||
describe('parseScriptingDefinition', () => {
|
describe('parseScriptingDefinition', () => {
|
||||||
|
it('validates data', () => {
|
||||||
|
// arrange
|
||||||
|
const data = new ScriptingDefinitionDataStub();
|
||||||
|
const expectedAssertion: ObjectAssertion<ScriptingDefinitionData> = {
|
||||||
|
value: data,
|
||||||
|
valueName: 'scripting definition',
|
||||||
|
allowedProperties: ['language', 'fileExtension', 'startCode', 'endCode'],
|
||||||
|
};
|
||||||
|
const validatorStub = new TypeValidatorStub();
|
||||||
|
const context = new TestContext()
|
||||||
|
.withTypeValidator(validatorStub)
|
||||||
|
.withData(data);
|
||||||
|
// act
|
||||||
|
context.parseScriptingDefinition();
|
||||||
|
// assert
|
||||||
|
validatorStub.assertObject(expectedAssertion);
|
||||||
|
});
|
||||||
describe('language', () => {
|
describe('language', () => {
|
||||||
it('parses as expected', () => {
|
it('parses as expected', () => {
|
||||||
// arrange
|
// arrange
|
||||||
const expectedLanguage = ScriptingLanguage.batchfile;
|
const expectedLanguage = ScriptingLanguage.batchfile;
|
||||||
const languageText = 'batchfile';
|
const languageText = 'batchfile';
|
||||||
const expectedName = 'language';
|
const expectedName = 'language';
|
||||||
const projectDetails = new ProjectDetailsStub();
|
|
||||||
const definition = new ScriptingDefinitionDataStub()
|
const definition = new ScriptingDefinitionDataStub()
|
||||||
.withLanguage(languageText);
|
.withLanguage(languageText);
|
||||||
const parserMock = new EnumParserStub<ScriptingLanguage>()
|
const parserMock = new EnumParserStub<ScriptingLanguage>()
|
||||||
.setup(expectedName, languageText, expectedLanguage);
|
.setup(expectedName, languageText, expectedLanguage);
|
||||||
const sut = new ScriptingDefinitionParserBuilder()
|
const context = new TestContext()
|
||||||
.withParser(parserMock)
|
.withParser(parserMock)
|
||||||
.build();
|
.withData(definition);
|
||||||
// act
|
// act
|
||||||
const actual = sut.parse(definition, projectDetails);
|
const actual = context.parseScriptingDefinition();
|
||||||
// assert
|
// assert
|
||||||
expect(actual.language).to.equal(expectedLanguage);
|
expect(actual.language).to.equal(expectedLanguage);
|
||||||
});
|
});
|
||||||
@@ -35,56 +55,92 @@ describe('ScriptingDefinitionParser', () => {
|
|||||||
// arrange
|
// arrange
|
||||||
const code = 'hello';
|
const code = 'hello';
|
||||||
const expected = 'substituted';
|
const expected = 'substituted';
|
||||||
const testCases = [
|
const testScenarios: readonly {
|
||||||
|
readonly description: string;
|
||||||
|
getActualValue(result: IScriptingDefinition): string;
|
||||||
|
readonly data: ScriptingDefinitionData;
|
||||||
|
}[] = [
|
||||||
{
|
{
|
||||||
name: 'startCode',
|
description: 'startCode',
|
||||||
getActualValue: (result: IScriptingDefinition) => result.startCode,
|
getActualValue: (result: IScriptingDefinition) => result.startCode,
|
||||||
data: new ScriptingDefinitionDataStub()
|
data: new ScriptingDefinitionDataStub()
|
||||||
.withStartCode(code),
|
.withStartCode(code),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'endCode',
|
description: 'endCode',
|
||||||
getActualValue: (result: IScriptingDefinition) => result.endCode,
|
getActualValue: (result: IScriptingDefinition) => result.endCode,
|
||||||
data: new ScriptingDefinitionDataStub()
|
data: new ScriptingDefinitionDataStub()
|
||||||
.withEndCode(code),
|
.withEndCode(code),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
for (const testCase of testCases) {
|
testScenarios.forEach(({
|
||||||
it(testCase.name, () => {
|
description, data, getActualValue,
|
||||||
|
}) => {
|
||||||
|
it(description, () => {
|
||||||
const projectDetails = new ProjectDetailsStub();
|
const projectDetails = new ProjectDetailsStub();
|
||||||
const substituterMock = new CodeSubstituterStub()
|
const substituterMock = new CodeSubstituterStub()
|
||||||
.setup(code, projectDetails, expected);
|
.setup(code, projectDetails, expected);
|
||||||
const sut = new ScriptingDefinitionParserBuilder()
|
const context = new TestContext()
|
||||||
.withSubstituter(substituterMock)
|
.withData(data)
|
||||||
.build();
|
.withProjectDetails(projectDetails)
|
||||||
|
.withSubstituter(substituterMock);
|
||||||
// act
|
// act
|
||||||
const definition = sut.parse(testCase.data, projectDetails);
|
const definition = context.parseScriptingDefinition();
|
||||||
// assert
|
// assert
|
||||||
const actual = testCase.getActualValue(definition);
|
const actual = getActualValue(definition);
|
||||||
expect(actual).to.equal(expected);
|
expect(actual).to.equal(expected);
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
class ScriptingDefinitionParserBuilder {
|
class TestContext {
|
||||||
private languageParser: IEnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
|
private languageParser: EnumParser<ScriptingLanguage> = new EnumParserStub<ScriptingLanguage>()
|
||||||
.setupDefaultValue(ScriptingLanguage.shellscript);
|
.setupDefaultValue(ScriptingLanguage.shellscript);
|
||||||
|
|
||||||
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
|
private codeSubstituter: ICodeSubstituter = new CodeSubstituterStub();
|
||||||
|
|
||||||
public withParser(parser: IEnumParser<ScriptingLanguage>) {
|
private validator: TypeValidator = new TypeValidatorStub();
|
||||||
|
|
||||||
|
private data: ScriptingDefinitionData = new ScriptingDefinitionDataStub();
|
||||||
|
|
||||||
|
private projectDetails: ProjectDetails = new ProjectDetailsStub();
|
||||||
|
|
||||||
|
public withData(data: ScriptingDefinitionData): this {
|
||||||
|
this.data = data;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withProjectDetails(projectDetails: ProjectDetails): this {
|
||||||
|
this.projectDetails = projectDetails;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public withParser(parser: EnumParser<ScriptingLanguage>): this {
|
||||||
this.languageParser = parser;
|
this.languageParser = parser;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public withSubstituter(substituter: ICodeSubstituter) {
|
public withSubstituter(substituter: ICodeSubstituter): this {
|
||||||
this.codeSubstituter = substituter;
|
this.codeSubstituter = substituter;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public build() {
|
public withTypeValidator(validator: TypeValidator): this {
|
||||||
return new ScriptingDefinitionParser(this.languageParser, this.codeSubstituter);
|
this.validator = validator;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public parseScriptingDefinition() {
|
||||||
|
return parseScriptingDefinition(
|
||||||
|
this.data,
|
||||||
|
this.projectDetails,
|
||||||
|
{
|
||||||
|
languageParser: this.languageParser,
|
||||||
|
codeSubstituter: this.codeSubstituter,
|
||||||
|
validator: this.validator,
|
||||||
|
},
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -318,6 +318,10 @@ class CategoryCollectionBuilder {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public construct(): CategoryCollection {
|
public construct(): CategoryCollection {
|
||||||
return new CategoryCollection(this.os, this.actions, this.scriptingDefinition);
|
return new CategoryCollection({
|
||||||
|
os: this.os,
|
||||||
|
actions: this.actions,
|
||||||
|
scripting: this.scriptingDefinition,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
23
tests/unit/shared/Stubs/CategoryCollectionFactoryStub.ts
Normal file
23
tests/unit/shared/Stubs/CategoryCollectionFactoryStub.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import type { CategoryCollectionFactory } from '@/application/Parser/CategoryCollectionParser';
|
||||||
|
import type { CategoryCollectionInitParameters } from '@/domain/CategoryCollection';
|
||||||
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||||
|
|
||||||
|
export function createCategoryCollectionFactorySpy(): {
|
||||||
|
readonly categoryCollectionFactorySpy: CategoryCollectionFactory;
|
||||||
|
getInitParameters: (
|
||||||
|
category: ICategoryCollection,
|
||||||
|
) => CategoryCollectionInitParameters | undefined;
|
||||||
|
} {
|
||||||
|
const createdCategoryCollections = new Map<
|
||||||
|
ICategoryCollection, CategoryCollectionInitParameters
|
||||||
|
>();
|
||||||
|
return {
|
||||||
|
categoryCollectionFactorySpy: (parameters) => {
|
||||||
|
const categoryCollection = new CategoryCollectionStub();
|
||||||
|
createdCategoryCollections.set(categoryCollection, parameters);
|
||||||
|
return categoryCollection;
|
||||||
|
},
|
||||||
|
getInitParameters: (category) => createdCategoryCollections.get(category),
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -2,8 +2,8 @@ import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
|||||||
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { getEnumValues } from '@/application/Common/Enum';
|
import { getEnumValues } from '@/application/Common/Enum';
|
||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import type { CategoryCollectionParserType } from '@/application/Parser/ApplicationParser';
|
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
|
import type { CategoryCollectionParser } from '@/application/Parser/CategoryCollectionParser';
|
||||||
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
import { CategoryCollectionStub } from './CategoryCollectionStub';
|
||||||
|
|
||||||
export class CategoryCollectionParserStub {
|
export class CategoryCollectionParserStub {
|
||||||
@@ -22,7 +22,7 @@ export class CategoryCollectionParserStub {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getStub(): CategoryCollectionParserType {
|
public getStub(): CategoryCollectionParser {
|
||||||
return (data: CollectionData, projectDetails: ProjectDetails): ICategoryCollection => {
|
return (data: CollectionData, projectDetails: ProjectDetails): ICategoryCollection => {
|
||||||
this.arguments.push({ data, projectDetails });
|
this.arguments.push({ data, projectDetails });
|
||||||
const foundReturnValue = this.returnValues.get(data);
|
const foundReturnValue = this.returnValues.get(data);
|
||||||
|
|||||||
34
tests/unit/shared/Stubs/CategoryParserStub.ts
Normal file
34
tests/unit/shared/Stubs/CategoryParserStub.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import type { CategoryParser } from '@/application/Parser/Executable/CategoryParser';
|
||||||
|
import type { CategoryData } from '@/application/collections/';
|
||||||
|
import type { Category } from '@/domain/Executables/Category/Category';
|
||||||
|
import type { CategoryCollectionSpecificUtilities } from '@/application/Parser/Executable/CategoryCollectionSpecificUtilities';
|
||||||
|
import { CategoryStub } from './CategoryStub';
|
||||||
|
|
||||||
|
export class CategoryParserStub {
|
||||||
|
private configuredParseResults = new Map<CategoryData, Category>();
|
||||||
|
|
||||||
|
private usedUtilities = new Array<CategoryCollectionSpecificUtilities>();
|
||||||
|
|
||||||
|
public get(): CategoryParser {
|
||||||
|
return (category, utilities) => {
|
||||||
|
const result = this.configuredParseResults.get(category);
|
||||||
|
this.usedUtilities.push(utilities);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return new CategoryStub(5489);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public withConfiguredParseResult(
|
||||||
|
givenCategory: CategoryData,
|
||||||
|
parsedCategory: Category,
|
||||||
|
): this {
|
||||||
|
this.configuredParseResults.set(givenCategory, parsedCategory);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public getUsedUtilities(): readonly CategoryCollectionSpecificUtilities[] {
|
||||||
|
return this.usedUtilities;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,17 @@
|
|||||||
import type { IEnumParser } from '@/application/Common/Enum';
|
import type { EnumParser } from '@/application/Common/Enum';
|
||||||
|
|
||||||
export class EnumParserStub<T> implements IEnumParser<T> {
|
export class EnumParserStub<T> implements EnumParser<T> {
|
||||||
private readonly scenarios = new Array<{
|
private readonly scenarios = new Array<{
|
||||||
inputName: string, inputValue: string, outputValue: T }>();
|
inputName: string, inputValue: string, outputValue: T }>();
|
||||||
|
|
||||||
private defaultValue: T;
|
private defaultValue: T;
|
||||||
|
|
||||||
public setup(inputName: string, inputValue: string, outputValue: T) {
|
public setup(inputName: string, inputValue: string, outputValue: T): this {
|
||||||
this.scenarios.push({ inputName, inputValue, outputValue });
|
this.scenarios.push({ inputName, inputValue, outputValue });
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public setupDefaultValue(outputValue: T) {
|
public setupDefaultValue(outputValue: T): this {
|
||||||
this.defaultValue = outputValue;
|
this.defaultValue = outputValue;
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
|
|
||||||
export const errorWithContextWrapperStub
|
export const errorWithContextWrapperStub
|
||||||
: ErrorWithContextWrapper = (error, message) => new Error(`[stubbed error wrapper] ${error.message} + ${message}`);
|
: ErrorWithContextWrapper = (error, message) => new Error(`[stubbed error wrapper] ${error.message} + ${message}`);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
import type { ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
||||||
|
|
||||||
export class ErrorWrapperStub {
|
export class ErrorWrapperStub {
|
||||||
private errorToReturn: Error | undefined;
|
private errorToReturn: Error | undefined;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ExecutableData } from '@/application/collections/';
|
|
||||||
import type { ExecutableValidator, ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
import type { ExecutableValidator, ExecutableValidatorFactory } from '@/application/Parser/Executable/Validation/ExecutableValidator';
|
||||||
|
import type { TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
export const createExecutableValidatorFactoryStub
|
export const createExecutableValidatorFactoryStub
|
||||||
@@ -23,10 +23,10 @@ export class ExecutableValidatorStub
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public assertDefined(data: ExecutableData): this {
|
public assertType(assert: (validator: TypeValidator) => void): this {
|
||||||
this.registerMethodCall({
|
this.registerMethodCall({
|
||||||
methodName: 'assertDefined',
|
methodName: 'assertType',
|
||||||
args: [data],
|
args: [assert],
|
||||||
});
|
});
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|||||||
45
tests/unit/shared/Stubs/TypeValidatorStub.ts
Normal file
45
tests/unit/shared/Stubs/TypeValidatorStub.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import type { NonEmptyCollectionAssertion, ObjectAssertion, TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
||||||
|
import type { FunctionKeys } from '@/TypeHelpers';
|
||||||
|
import { expectDeepIncludes } from '@tests/shared/Assertions/ExpectDeepIncludes';
|
||||||
|
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||||
|
|
||||||
|
export type UnknownObjectAssertion = ObjectAssertion<unknown>;
|
||||||
|
|
||||||
|
export class TypeValidatorStub
|
||||||
|
extends StubWithObservableMethodCalls<TypeValidator>
|
||||||
|
implements TypeValidator {
|
||||||
|
public assertObject<T>(assertion: ObjectAssertion<T>): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'assertObject',
|
||||||
|
args: [assertion as UnknownObjectAssertion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void {
|
||||||
|
this.registerMethodCall({
|
||||||
|
methodName: 'assertNonEmptyCollection',
|
||||||
|
args: [assertion],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectObjectAssertion<T>(
|
||||||
|
expectedAssertion: ObjectAssertion<T>,
|
||||||
|
): void {
|
||||||
|
this.expectAssertion('assertObject', expectedAssertion as UnknownObjectAssertion);
|
||||||
|
}
|
||||||
|
|
||||||
|
public expectNonEmptyCollectionAssertion(
|
||||||
|
expectedAssertion: NonEmptyCollectionAssertion,
|
||||||
|
): void {
|
||||||
|
this.expectAssertion('assertNonEmptyCollection', expectedAssertion);
|
||||||
|
}
|
||||||
|
|
||||||
|
private expectAssertion<T extends FunctionKeys<TypeValidator>>(
|
||||||
|
methodName: T,
|
||||||
|
expectedAssertion: Parameters<TypeValidator[T]>[0],
|
||||||
|
): void {
|
||||||
|
const assertionCalls = this.callHistory.filter((c) => c.methodName === methodName);
|
||||||
|
const assertionArgs = assertionCalls.map((c) => c.args[0]);
|
||||||
|
expectDeepIncludes(assertionArgs, expectedAssertion);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user