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:
@@ -1,40 +1,48 @@
|
||||
import type { CollectionData } from '@/application/collections/';
|
||||
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 MacOsData from '@/application/collections/macos.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 type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
|
||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||
|
||||
export function parseApplication(
|
||||
categoryParser = parseCategoryCollection,
|
||||
projectDetailsParser = parseProjectDetails,
|
||||
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||
collectionsData = PreParsedCollections,
|
||||
collectionsData: readonly CollectionData[] = PreParsedCollections,
|
||||
utilities: ApplicationParserUtilities = DefaultUtilities,
|
||||
): IApplication {
|
||||
validateCollectionsData(collectionsData);
|
||||
const projectDetails = projectDetailsParser(metadata);
|
||||
validateCollectionsData(collectionsData, utilities.validator);
|
||||
const projectDetails = utilities.parseProjectDetails();
|
||||
const collections = collectionsData.map(
|
||||
(collection) => categoryParser(collection, projectDetails),
|
||||
(collection) => utilities.parseCategoryCollection(collection, projectDetails),
|
||||
);
|
||||
const app = new Application(projectDetails, collections);
|
||||
return app;
|
||||
}
|
||||
|
||||
export type CategoryCollectionParserType
|
||||
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
|
||||
|
||||
const PreParsedCollections: readonly CollectionData [] = [
|
||||
const PreParsedCollections: readonly CollectionData[] = [
|
||||
WindowsData, MacOsData, LinuxData,
|
||||
];
|
||||
|
||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||
if (!collections.length) {
|
||||
throw new Error('missing collections');
|
||||
}
|
||||
function validateCollectionsData(
|
||||
collections: readonly CollectionData[],
|
||||
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 { CategoryCollection } from '@/domain/CategoryCollection';
|
||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { parseCategory } from './Executable/CategoryParser';
|
||||
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
||||
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
||||
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
||||
|
||||
export function parseCategoryCollection(
|
||||
content: CollectionData,
|
||||
projectDetails: ProjectDetails,
|
||||
osParser = createEnumParser(OperatingSystem),
|
||||
createUtilities: CategoryCollectionSpecificUtilitiesFactory = createCollectionUtilities,
|
||||
): 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,
|
||||
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,
|
||||
projectDetails: ProjectDetails,
|
||||
utilities?: CategoryCollectionParserUtilities,
|
||||
): ICategoryCollection;
|
||||
}
|
||||
|
||||
function validate(content: CollectionData): void {
|
||||
if (!content.actions.length) {
|
||||
throw new Error('content does not define any action');
|
||||
}
|
||||
function validateCollection(
|
||||
content: CollectionData,
|
||||
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 {
|
||||
CategoryData, ScriptData, ExecutableData,
|
||||
} 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 { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
|
||||
import type { Script } from '@/domain/Executables/Script/Script';
|
||||
@@ -13,16 +13,24 @@ import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSp
|
||||
|
||||
let categoryIdCounter = 0;
|
||||
|
||||
export function parseCategory(
|
||||
export const parseCategory: CategoryParser = (
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||
): Category {
|
||||
) => {
|
||||
return parseCategoryRecursively({
|
||||
categoryData: category,
|
||||
collectionUtilities,
|
||||
categoryUtilities,
|
||||
});
|
||||
};
|
||||
|
||||
export interface CategoryParser {
|
||||
(
|
||||
category: CategoryData,
|
||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
||||
categoryUtilities?: CategoryParserUtilities,
|
||||
): Category;
|
||||
}
|
||||
|
||||
interface CategoryParseContext {
|
||||
@@ -41,7 +49,7 @@ function parseCategoryRecursively(
|
||||
subscripts: new Array<Script>(),
|
||||
};
|
||||
for (const data of context.categoryData.children) {
|
||||
parseExecutable({
|
||||
parseUnknownExecutable({
|
||||
data,
|
||||
children,
|
||||
parent: context.categoryData,
|
||||
@@ -74,12 +82,18 @@ function ensureValidCategory(
|
||||
self: context.categoryData,
|
||||
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.assert(
|
||||
() => Boolean(category.children) && category.children.length > 0,
|
||||
`"${category.category}" has no children.`,
|
||||
);
|
||||
validator.assertType((v) => v.assertNonEmptyCollection({
|
||||
value: category.children,
|
||||
valueName: category.category,
|
||||
}));
|
||||
return validator;
|
||||
}
|
||||
|
||||
@@ -96,12 +110,15 @@ interface ExecutableParseContext {
|
||||
readonly categoryUtilities: CategoryParserUtilities;
|
||||
}
|
||||
|
||||
function parseExecutable(context: ExecutableParseContext) {
|
||||
function parseUnknownExecutable(context: ExecutableParseContext) {
|
||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
||||
self: context.data,
|
||||
parentCategory: context.parent,
|
||||
});
|
||||
validator.assertDefined(context.data);
|
||||
validator.assertType((v) => v.assertObject({
|
||||
value: context.data,
|
||||
valueName: 'Executable',
|
||||
}));
|
||||
validator.assert(
|
||||
() => isCategory(context.data) || isScript(context.data),
|
||||
'Executable is neither a category or a script.',
|
||||
|
||||
@@ -50,5 +50,5 @@ class DocumentationContainer {
|
||||
}
|
||||
|
||||
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 { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||
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 { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||
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';
|
||||
|
||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
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 { 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 type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||
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 type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
||||
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 { SharedFunctionCollection } from './SharedFunctionCollection';
|
||||
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 { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
||||
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 { SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
||||
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 type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
||||
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 { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
||||
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 { ExecutableType } from '../Validation/ExecutableType';
|
||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
||||
@@ -28,23 +28,28 @@ export interface ScriptParser {
|
||||
export const parseScript: ScriptParser = (
|
||||
data,
|
||||
collectionUtilities,
|
||||
utilities = DefaultScriptParserUtilities,
|
||||
scriptUtilities = DefaultUtilities,
|
||||
) => {
|
||||
const validator = utilities.createValidator({
|
||||
const validator = scriptUtilities.createValidator({
|
||||
type: ExecutableType.Script,
|
||||
self: data,
|
||||
});
|
||||
validateScript(data, validator);
|
||||
try {
|
||||
const script = utilities.createScript({
|
||||
const script = scriptUtilities.createScript({
|
||||
name: data.name,
|
||||
code: parseCode(data, collectionUtilities, utilities.codeValidator, utilities.createCode),
|
||||
docs: utilities.parseDocs(data),
|
||||
level: parseLevel(data.recommend, utilities.levelParser),
|
||||
code: parseCode(
|
||||
data,
|
||||
collectionUtilities,
|
||||
scriptUtilities.codeValidator,
|
||||
scriptUtilities.createCode,
|
||||
),
|
||||
docs: scriptUtilities.parseDocs(data),
|
||||
level: parseLevel(data.recommend, scriptUtilities.levelParser),
|
||||
});
|
||||
return script;
|
||||
} catch (error) {
|
||||
throw utilities.wrapError(
|
||||
throw scriptUtilities.wrapError(
|
||||
error,
|
||||
validator.createContextualErrorMessage('Failed to parse script.'),
|
||||
);
|
||||
@@ -53,7 +58,7 @@ export const parseScript: ScriptParser = (
|
||||
|
||||
function parseLevel(
|
||||
level: string | undefined,
|
||||
parser: IEnumParser<RecommendationLevel>,
|
||||
parser: EnumParser<RecommendationLevel>,
|
||||
): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
@@ -95,7 +100,13 @@ function validateScript(
|
||||
script: ScriptData,
|
||||
validator: ExecutableValidator,
|
||||
): 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.assert(
|
||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||
@@ -112,7 +123,7 @@ function validateScript(
|
||||
}
|
||||
|
||||
interface ScriptParserUtilities {
|
||||
readonly levelParser: IEnumParser<RecommendationLevel>;
|
||||
readonly levelParser: EnumParser<RecommendationLevel>;
|
||||
readonly createScript: ScriptFactory;
|
||||
readonly codeValidator: ICodeValidator;
|
||||
readonly wrapError: ErrorWithContextWrapper;
|
||||
@@ -129,7 +140,7 @@ const createScript: ScriptFactory = (...parameters) => {
|
||||
return new CollectionScript(...parameters);
|
||||
};
|
||||
|
||||
const DefaultScriptParserUtilities: ScriptParserUtilities = {
|
||||
const DefaultUtilities: ScriptParserUtilities = {
|
||||
levelParser: createEnumParser(RecommendationLevel),
|
||||
createScript,
|
||||
codeValidator: CodeValidator.instance,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { isString } from '@/TypeHelpers';
|
||||
import type { ExecutableData } from '@/application/collections/';
|
||||
import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator';
|
||||
import { type ExecutableErrorContext } from './ExecutableErrorContext';
|
||||
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
|
||||
|
||||
@@ -7,11 +7,11 @@ export interface ExecutableValidatorFactory {
|
||||
(context: ExecutableErrorContext): ExecutableValidator;
|
||||
}
|
||||
|
||||
type AssertTypeFunction = (validator: TypeValidator) => void;
|
||||
|
||||
export interface ExecutableValidator {
|
||||
assertValidName(nameValue: string): void;
|
||||
assertDefined(
|
||||
data: ExecutableData | undefined,
|
||||
): asserts data is NonNullable<ExecutableData> & void;
|
||||
assertType(assert: AssertTypeFunction): void;
|
||||
assert(
|
||||
validationPredicate: () => boolean,
|
||||
errorMessage: string,
|
||||
@@ -27,6 +27,7 @@ export class ContextualExecutableValidator implements ExecutableValidator {
|
||||
private readonly context: ExecutableErrorContext,
|
||||
private readonly createErrorMessage
|
||||
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
|
||||
private readonly validator: TypeValidator = createTypeValidator(),
|
||||
) {
|
||||
|
||||
}
|
||||
@@ -39,13 +40,12 @@ export class ContextualExecutableValidator implements ExecutableValidator {
|
||||
);
|
||||
}
|
||||
|
||||
public assertDefined(
|
||||
data: ExecutableData,
|
||||
): asserts data is NonNullable<ExecutableData> {
|
||||
this.assert(
|
||||
() => data !== undefined && data !== null && Object.keys(data).length > 0,
|
||||
'missing executable data',
|
||||
);
|
||||
public assertType(assert: AssertTypeFunction): void {
|
||||
try {
|
||||
assert(this.validator);
|
||||
} catch (error) {
|
||||
this.throw(error.message);
|
||||
}
|
||||
}
|
||||
|
||||
public assert(
|
||||
|
||||
@@ -24,6 +24,10 @@ parseProjectDetails(
|
||||
);
|
||||
}
|
||||
|
||||
export interface ProjectDetailsParser {
|
||||
(): ProjectDetails;
|
||||
}
|
||||
|
||||
export type ProjectDetailsFactory = (
|
||||
...args: ConstructorArguments<typeof GitHubProjectDetails>
|
||||
) => ProjectDetails;
|
||||
|
||||
@@ -3,28 +3,54 @@ import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
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 type { ICodeSubstituter } from './ICodeSubstituter';
|
||||
|
||||
export class ScriptingDefinitionParser {
|
||||
constructor(
|
||||
private readonly languageParser = createEnumParser(ScriptingLanguage),
|
||||
private readonly codeSubstituter: ICodeSubstituter = new CodeSubstituter(),
|
||||
) {
|
||||
}
|
||||
export const parseScriptingDefinition: ScriptingDefinitionParser = (
|
||||
definition,
|
||||
projectDetails,
|
||||
utilities: ScriptingDefinitionParserUtilities = DefaultUtilities,
|
||||
) => {
|
||||
validateData(definition, utilities.validator);
|
||||
const language = utilities.languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = utilities.codeSubstituter.substitute(definition.startCode, projectDetails);
|
||||
const endCode = utilities.codeSubstituter.substitute(definition.endCode, projectDetails);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
};
|
||||
|
||||
public parse(
|
||||
export interface ScriptingDefinitionParser {
|
||||
(
|
||||
definition: ScriptingDefinitionData,
|
||||
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(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
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(),
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user