diff --git a/docs/application.md b/docs/application.md index 6e2eb4f87..8ea9ba6fd 100644 --- a/docs/application.md +++ b/docs/application.md @@ -3,7 +3,7 @@ Application layer is mainly responsible for: - creating an event-based and mutable [application state](#application-state), -- [parsing and compiling](#parsing-and-compiling) the [application data](#application-data). +- [compiling](#compiling) the [application data](#application-data). 📖 Refer to [architecture.md | Layered Application](./architecture.md#layered-application) to read more about the layered architecture. @@ -27,17 +27,27 @@ Presentation layer uses a singleton (same instance of) [`ApplicationContext.ts`] Application data is collection files using YAML. You can refer to [collection-files.md](./collection-files.md) to read more about the scheme and structure of application data files. You can also check the source code [collection yaml files](./../src/application/collections/) to directly see the application data using that scheme. -Application layer [parses and compiles](#parsing-and-compiling) application data into [`Application`](./../src/domain/Application.ts)). Once parsed, application layer provides the necessary functionality to presentation layer based on the application data. You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data). +Application layer loads and [compiles](#compiling) application data into [`Application`](./../src/domain/Application.ts). +Once loaded, application layer provides the necessary functionality to presentation layer based on the application data. +You can read more about how presentation layer consumes the application data in [presentation.md | Application Data](./presentation.md#application-data). Application layer enables [data-driven programming](https://en.wikipedia.org/wiki/Data-driven_programming) by leveraging the data to the rest of the source code. It makes it easy for community to contribute on the project by using a declarative language used in collection files. -### Parsing and compiling +### Compiling -Application layer parses the application data to compile the domain object [`Application.ts`](./../src/domain/Application.ts). +Application layer loads the application data, compiles it, and makes it available as the domain object [`Application.ts`](./../src/domain/Application.ts). -The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. Application layer ([`ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts)) parses and compiles this data in runtime. +The process looks like this: -Application layer compiles templating syntax during parsing to create the end scripts. You can read more about templating syntax in [templating.md](./templating.md) and how application data uses them through functions in [collection-files.md | Function](./collection-files.md#function). +1. *(Compile time)* + The build tool loads (or injects) application data ([collection yaml files](./../src/application/collections/)) into the application layer in compile time. + See [`PreloadedCollectionDataProvider.ts`](./../src/application/PreloadedCollectionDataProvider.ts) +2. *(Runtime)* + Compiler compiles the data into a data transfer object (DTO). + Compiler compiles templating syntax during parsing to create the end scripts + See [`Compiler/`](./../src/application/Compiler/), [templating.md](./templating.md), [collection-files.md](./collection-files.md). +3. *(Runtime)* + Application layer ([`ApplicationProvider.ts`](./../src/application/Loader/ApplicationProvider.ts)) provides this application object. The steps to extend the templating syntax: diff --git a/src/application/ApplicationFactory.ts b/src/application/ApplicationFactory.ts deleted file mode 100644 index 9c851a524..000000000 --- a/src/application/ApplicationFactory.ts +++ /dev/null @@ -1,21 +0,0 @@ -import type { IApplication } from '@/domain/IApplication'; -import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; -import { parseApplication } from './Parser/ApplicationParser'; -import type { IApplicationFactory } from './IApplicationFactory'; - -export type ApplicationGetterType = () => IApplication; -const ApplicationGetter: ApplicationGetterType = parseApplication; - -export class ApplicationFactory implements IApplicationFactory { - public static readonly Current: IApplicationFactory = new ApplicationFactory(ApplicationGetter); - - private readonly getter: AsyncLazy; - - protected constructor(costlyGetter: ApplicationGetterType) { - this.getter = new AsyncLazy(() => Promise.resolve(costlyGetter())); - } - - public getApp(): Promise { - return this.getter.getValue(); - } -} diff --git a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts b/src/application/Compiler/Collection/ScriptingDefinition/CodeSubstituter.ts similarity index 95% rename from src/application/Parser/ScriptingDefinition/CodeSubstituter.ts rename to src/application/Compiler/Collection/ScriptingDefinition/CodeSubstituter.ts index 20f52ec93..d8486e853 100644 --- a/src/application/Parser/ScriptingDefinition/CodeSubstituter.ts +++ b/src/application/Compiler/Collection/ScriptingDefinition/CodeSubstituter.ts @@ -5,7 +5,7 @@ import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Comp import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser'; -import { createFunctionCallArgument, type FunctionCallArgumentFactory } from '../Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; +import { createFunctionCallArgument, type FunctionCallArgumentFactory } from '../../../Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; export interface CodeSubstituter { ( diff --git a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts b/src/application/Compiler/Collection/ScriptingDefinition/ScriptingDefinitionParser.ts similarity index 78% rename from src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts rename to src/application/Compiler/Collection/ScriptingDefinition/ScriptingDefinitionParser.ts index 838769904..fcc749453 100644 --- a/src/application/Parser/ScriptingDefinition/ScriptingDefinitionParser.ts +++ b/src/application/Compiler/Collection/ScriptingDefinition/ScriptingDefinitionParser.ts @@ -1,13 +1,13 @@ import type { ScriptingDefinitionData } from '@/application/collections/'; -import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; -import { ScriptingDefinition } from '@/domain/ScriptingDefinition'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; +import { ScriptingDefinition } from '@/domain/ScriptingDefinitionFactory'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; -import { createEnumParser, type EnumParser } from '../../Common/Enum'; -import { createTypeValidator, type TypeValidator } from '../Common/TypeValidator'; +import { createEnumParser, type EnumParser } from '../../../Common/Enum'; +import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator'; import { type CodeSubstituter, substituteCode } from './CodeSubstituter'; -export const parseScriptingDefinition: ScriptingDefinitionParser = ( +export const parseScriptingDefinition: ScriptingDefinitionCompiler = ( definition, projectDetails, utilities: ScriptingDefinitionParserUtilities = DefaultUtilities, @@ -16,19 +16,19 @@ export const parseScriptingDefinition: ScriptingDefinitionParser = ( const language = utilities.languageParser.parseEnum(definition.language, 'language'); const startCode = utilities.codeSubstituter(definition.startCode, projectDetails); const endCode = utilities.codeSubstituter(definition.endCode, projectDetails); - return new ScriptingDefinition( + return new ScriptingDefinitionDto( language, startCode, endCode, ); }; -export interface ScriptingDefinitionParser { +export interface ScriptingDefinitionCompiler { ( definition: ScriptingDefinitionData, projectDetails: ProjectDetails, utilities?: ScriptingDefinitionParserUtilities, - ): IScriptingDefinition; + ): ScriptingDefinition; } function validateData( diff --git a/src/application/Compiler/Collection/ScriptingDefinitionCompiler.ts b/src/application/Compiler/Collection/ScriptingDefinitionCompiler.ts new file mode 100644 index 000000000..e69de29bb diff --git a/src/application/Compiler/Collection/SingleCollectionCompiler.ts b/src/application/Compiler/Collection/SingleCollectionCompiler.ts new file mode 100644 index 000000000..eac85c393 --- /dev/null +++ b/src/application/Compiler/Collection/SingleCollectionCompiler.ts @@ -0,0 +1,55 @@ +import type { CollectionData } from '@/application/collections/'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import type { CompiledCollectionDto } from '../CompiledCollectionDto'; +import TypeValidator, { createTypeValidator } from '@/application/Compiler/Common/TypeValidator'; + +export interface SingleCollectionCompiler { //TODO: CategoryCollectionParser will become this. + ( + collectionData: CollectionData, + projectDetails: ProjectDetails, + utilities?: SingleCollectionCompilerUtilities, + ): CompiledCollectionDto; +} + +interface SingleCollectionCompilerUtilities { + readonly osParser: EnumParser; + readonly validator: TypeValidator; + readonly parseScriptingDefinition: ScriptingDefinitionParser; + readonly createContext: CategoryCollectionContextFactory; + readonly parseCategory: CategoryParser; + readonly createCategoryCollection: CategoryCollectionFactory; +} + +export const compileCollection: SingleCollectionCompiler = ( + collectionData, + projectDetails, + utilities = DefaultUtilities, +) => { + validateCollection(content, utilities.validator); +}; + +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', + }); +} + +const DefaultUtilities: CategoryCollectionParserUtilities = { + validator: createTypeValidator(), + osParser: createEnumParser(OperatingSystem), + parseScriptingDefinition, + createContext: createCategoryCollectionContext, + parseCategory, + createCategoryCollection, +}; diff --git a/src/application/Parser/Common/ContextualError.ts b/src/application/Compiler/Common/ContextualError.ts similarity index 100% rename from src/application/Parser/Common/ContextualError.ts rename to src/application/Compiler/Common/ContextualError.ts diff --git a/src/application/Parser/Common/TypeValidator.ts b/src/application/Compiler/Common/TypeValidator.ts similarity index 100% rename from src/application/Parser/Common/TypeValidator.ts rename to src/application/Compiler/Common/TypeValidator.ts diff --git a/src/application/Compiler/CompiledCollectionDto.ts b/src/application/Compiler/CompiledCollectionDto.ts new file mode 100644 index 000000000..878fa6ba4 --- /dev/null +++ b/src/application/Compiler/CompiledCollectionDto.ts @@ -0,0 +1,23 @@ +export interface CompiledCollectionDto { + readonly os: string; + readonly startCode: string; + readonly endCode: string; + readonly language: string; + readonly rootCategories: readonly CompiledCategoryDto[]; +} + +export interface CompiledExecutableDto { + readonly markdownDocs: string; + readonly title: string; + readonly executableId: string; +} + +export interface CompiledScriptDto extends CompiledExecutableDto { + readonly code: string; + readonly revertCode: string; +} + +export interface CompiledCategoryDto extends CompiledExecutableDto { + readonly categories: readonly CompiledCategoryDto[]; + readonly scripts: readonly CompiledScriptDto[]; +} diff --git a/src/application/Compiler/MultipleCollectionsCompiler.ts b/src/application/Compiler/MultipleCollectionsCompiler.ts new file mode 100644 index 000000000..0c69ac868 --- /dev/null +++ b/src/application/Compiler/MultipleCollectionsCompiler.ts @@ -0,0 +1,54 @@ +import type { CollectionData } from '@/application/collections/'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import { createTypeValidator, type TypeValidator } from './Common/TypeValidator'; +import { compileCollection, type SingleCollectionCompiler } from './Collection/SingleCollectionCompiler'; +import type { CompiledCollectionDto } from './CompiledCollectionDto'; + +/** + * This utility is responsible for compiling collection data into an application object. + * It serves as part of the application layer, parsing and compiling the application data + * defined in collection YAML files. + * + * The compiler creates basic, decoupled abstractions without external references, + * acting as an anti-corruption layer. This design allows for independent changes + * to the compiler and its models. + */ +export interface MultipleCollectionsCompiler { + ( + collectionsData: readonly CollectionData[], + projectDetails: ProjectDetails, + utilities?: CollectionCompilerUtilities, + ): CompiledCollectionDto[]; +} + +export const compileCollections: MultipleCollectionsCompiler = ( + collectionsData, + projectDetails, + utilities = DefaultUtilities, +) => { + validateCollectionsData(collectionsData, utilities.validator); + const collections = collectionsData.map( + (collection) => utilities.compileCollection(collection, projectDetails), + ); + return collections; +}; + +interface CollectionCompilerUtilities { + readonly validator: TypeValidator; + readonly compileCollection: SingleCollectionCompiler; +} + +const DefaultUtilities: CollectionCompilerUtilities = { + validator: createTypeValidator(), + compileCollection, +}; + +function validateCollectionsData( + collections: readonly CollectionData[], + validator: TypeValidator, +) { + validator.assertNonEmptyCollection({ + value: collections, + valueName: 'Collections', + }); +} diff --git a/src/application/Context/ApplicationContext.ts b/src/application/Context/ApplicationContext.ts index 7c9cd289b..e0a50c68b 100644 --- a/src/application/Context/ApplicationContext.ts +++ b/src/application/Context/ApplicationContext.ts @@ -1,6 +1,6 @@ -import type { IApplication } from '@/domain/IApplication'; +import type { Application } from '@/domain/Application'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { EventSource } from '@/infrastructure/Events/EventSource'; import { assertInRange } from '@/application/Common/Enum'; import { CategoryCollectionState } from './State/CategoryCollectionState'; @@ -12,7 +12,7 @@ type StateMachine = Map; export class ApplicationContext implements IApplicationContext { public readonly contextChanged = new EventSource(); - public collection: ICategoryCollection; + public collection: CategoryCollection; public currentOs: OperatingSystem; @@ -23,7 +23,7 @@ export class ApplicationContext implements IApplicationContext { private readonly states: StateMachine; public constructor( - public readonly app: IApplication, + public readonly app: Application, initialContext: OperatingSystem, ) { this.setContext(initialContext); @@ -59,7 +59,7 @@ export class ApplicationContext implements IApplicationContext { function validateOperatingSystem( os: OperatingSystem, - app: IApplication, + app: Application, ): void { assertInRange(os, OperatingSystem); if (!app.getSupportedOsList().includes(os)) { @@ -67,7 +67,7 @@ function validateOperatingSystem( } } -function initializeStates(app: IApplication): StateMachine { +function initializeStates(app: Application): StateMachine { const machine = new Map(); for (const collection of app.collections) { machine.set(collection.os, new CategoryCollectionState(collection)); diff --git a/src/application/Context/ApplicationContextFactory.ts b/src/application/Context/ApplicationContextFactory.ts index 99f4344f0..ac236245a 100644 --- a/src/application/Context/ApplicationContextFactory.ts +++ b/src/application/Context/ApplicationContextFactory.ts @@ -1,22 +1,22 @@ import type { IApplicationContext } from '@/application/Context/IApplicationContext'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import type { IApplication } from '@/domain/IApplication'; +import type { Application } from '@/domain/Application'; import { CurrentEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironmentFactory'; -import { ApplicationFactory } from '../ApplicationFactory'; +import { createOrGetApplication } from '../Loader/LazySingletonApplicationProvider'; import { ApplicationContext } from './ApplicationContext'; -import type { IApplicationFactory } from '../IApplicationFactory'; +import type { ApplicationProvider } from '../Loader/ApplicationProvider'; export async function buildContext( - factory: IApplicationFactory = ApplicationFactory.Current, + getApplication: ApplicationProvider = createOrGetApplication, environment = CurrentEnvironment, ): Promise { - const app = await factory.getApp(); + const app = await getApplication(); const os = getInitialOs(app, environment.os); return new ApplicationContext(app, os); } function getInitialOs( - app: IApplication, + app: Application, currentOs: OperatingSystem | undefined, ): OperatingSystem { const supportedOsList = app.getSupportedOsList(); @@ -26,7 +26,7 @@ function getInitialOs( return getMostSupportedOs(supportedOsList, app); } -function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) { +function getMostSupportedOs(supportedOsList: OperatingSystem[], app: Application) { supportedOsList.sort((os1, os2) => { const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts; return getPriority(os2) - getPriority(os1); diff --git a/src/application/Context/IApplicationContext.ts b/src/application/Context/IApplicationContext.ts index adabcf621..fa0627b85 100644 --- a/src/application/Context/IApplicationContext.ts +++ b/src/application/Context/IApplicationContext.ts @@ -1,10 +1,10 @@ import { OperatingSystem } from '@/domain/OperatingSystem'; import type { IEventSource } from '@/infrastructure/Events/IEventSource'; -import type { IApplication } from '@/domain/IApplication'; +import type { Application } from '@/domain/Application'; import type { ICategoryCollectionState, IReadOnlyCategoryCollectionState } from './State/ICategoryCollectionState'; export interface IReadOnlyApplicationContext { - readonly app: IApplication; + readonly app: Application; readonly state: IReadOnlyCategoryCollectionState; readonly contextChanged: IEventSource; } diff --git a/src/application/Context/State/CategoryCollectionState.ts b/src/application/Context/State/CategoryCollectionState.ts index 6d37ed8fc..fef43d061 100644 --- a/src/application/Context/State/CategoryCollectionState.ts +++ b/src/application/Context/State/CategoryCollectionState.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext'; import { ApplicationCode } from './Code/ApplicationCode'; @@ -18,7 +18,7 @@ export class CategoryCollectionState implements ICategoryCollectionState { public readonly filter: FilterContext; public constructor( - public readonly collection: ICategoryCollection, + public readonly collection: CategoryCollection, selectionFactory = DefaultSelectionFactory, codeFactory = DefaultCodeFactory, filterFactory = DefaultFilterFactory, diff --git a/src/application/Context/State/Code/ApplicationCode.ts b/src/application/Context/State/Code/ApplicationCode.ts index e376aadb4..1882509fe 100644 --- a/src/application/Context/State/Code/ApplicationCode.ts +++ b/src/application/Context/State/Code/ApplicationCode.ts @@ -1,5 +1,5 @@ import { EventSource } from '@/infrastructure/Events/EventSource'; -import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection'; import { CodeChangedEvent } from './Event/CodeChangedEvent'; @@ -18,7 +18,7 @@ export class ApplicationCode implements IApplicationCode { constructor( selection: ReadonlyScriptSelection, - private readonly scriptingDefinition: IScriptingDefinition, + private readonly scriptingDefinition: ScriptingDefinition, private readonly generator: IUserScriptGenerator = new UserScriptGenerator(), ) { this.setCode(selection.selectedScripts); diff --git a/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts b/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts index 4ea291335..28018abf4 100644 --- a/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/IUserScriptGenerator.ts @@ -1,10 +1,10 @@ -import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import type { IUserScript } from './IUserScript'; export interface IUserScriptGenerator { buildCode( selectedScripts: ReadonlyArray, - scriptingDefinition: IScriptingDefinition, + scriptingDefinition: ScriptingDefinition, ): IUserScript; } diff --git a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts index 042a20efb..3ba64bc97 100644 --- a/src/application/Context/State/Code/Generation/UserScriptGenerator.ts +++ b/src/application/Context/State/Code/Generation/UserScriptGenerator.ts @@ -1,5 +1,5 @@ import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition'; -import type { IScriptingDefinition } from '@/domain/IScriptingDefinition'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript'; import { CodePosition } from '../Position/CodePosition'; import { CodeBuilderFactory } from './CodeBuilderFactory'; @@ -15,7 +15,7 @@ export class UserScriptGenerator implements IUserScriptGenerator { public buildCode( selectedScripts: ReadonlyArray, - scriptingDefinition: IScriptingDefinition, + scriptingDefinition: ScriptingDefinition, ): IUserScript { if (!selectedScripts.length) { return { code: '', scriptPositions: new Map() }; diff --git a/src/application/Context/State/Filter/AdaptiveFilterContext.ts b/src/application/Context/State/Filter/AdaptiveFilterContext.ts index 6ecba4619..c8d90a382 100644 --- a/src/application/Context/State/Filter/AdaptiveFilterContext.ts +++ b/src/application/Context/State/Filter/AdaptiveFilterContext.ts @@ -1,5 +1,5 @@ import { EventSource } from '@/infrastructure/Events/EventSource'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { FilterChange } from './Event/FilterChange'; import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy'; import type { FilterResult } from './Result/FilterResult'; @@ -13,7 +13,7 @@ export class AdaptiveFilterContext implements FilterContext { public currentFilter: FilterResult | undefined; constructor( - private readonly collection: ICategoryCollection, + private readonly collection: CategoryCollection, private readonly filterStrategy: FilterStrategy = new LinearFilterStrategy(), ) { diff --git a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts index 2a4a7862f..2f7a51fee 100644 --- a/src/application/Context/State/Filter/Strategy/FilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/FilterStrategy.ts @@ -1,9 +1,9 @@ -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import type { FilterResult } from '../Result/FilterResult'; export interface FilterStrategy { applyFilter( filter: string, - collection: ICategoryCollection, + collection: CategoryCollection, ): FilterResult; } diff --git a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts index 327bdf807..75e23b2e4 100644 --- a/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts +++ b/src/application/Context/State/Filter/Strategy/LinearFilterStrategy.ts @@ -1,14 +1,14 @@ import type { Category } from '@/domain/Executables/Category/Category'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import type { Documentable } from '@/domain/Executables/Documentable'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import type { Script } from '@/domain/Executables/Script/Script'; import { AppliedFilterResult } from '../Result/AppliedFilterResult'; import type { FilterStrategy } from './FilterStrategy'; import type { FilterResult } from '../Result/FilterResult'; export class LinearFilterStrategy implements FilterStrategy { - applyFilter(filter: string, collection: ICategoryCollection): FilterResult { + applyFilter(filter: string, collection: CategoryCollection): FilterResult { const filterLowercase = filter.toLocaleLowerCase(); const filteredScripts = collection.getAllScripts().filter( (script) => matchesScript(script, filterLowercase), diff --git a/src/application/Context/State/ICategoryCollectionState.ts b/src/application/Context/State/ICategoryCollectionState.ts index 3d9bdaa57..607f83504 100644 --- a/src/application/Context/State/ICategoryCollectionState.ts +++ b/src/application/Context/State/ICategoryCollectionState.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { OperatingSystem } from '@/domain/OperatingSystem'; import type { IApplicationCode } from './Code/IApplicationCode'; import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext'; @@ -9,7 +9,7 @@ export interface IReadOnlyCategoryCollectionState { readonly os: OperatingSystem; readonly filter: ReadonlyFilterContext; readonly selection: ReadonlyUserSelection; - readonly collection: ICategoryCollection; + readonly collection: CategoryCollection; } export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState { diff --git a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts index a2916926c..062ac76ea 100644 --- a/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts +++ b/src/application/Context/State/Selection/Category/ScriptToCategorySelectionMapper.ts @@ -1,5 +1,5 @@ import type { Category } from '@/domain/Executables/Category/Category'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelection } from './CategorySelection'; import type { ScriptSelection } from '../Script/ScriptSelection'; @@ -8,7 +8,7 @@ import type { ScriptSelectionChange } from '../Script/ScriptSelectionChange'; export class ScriptToCategorySelectionMapper implements CategorySelection { constructor( private readonly scriptSelection: ScriptSelection, - private readonly collection: ICategoryCollection, + private readonly collection: CategoryCollection, ) { } diff --git a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts index 9898b82f5..7a7e9ddf8 100644 --- a/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts +++ b/src/application/Context/State/Selection/Script/DebouncedScriptSelection.ts @@ -2,7 +2,7 @@ import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryReposito import type { Script } from '@/domain/Executables/Script/Script'; import { EventSource } from '@/infrastructure/Events/EventSource'; import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce'; import type { ExecutableId } from '@/domain/Executables/Identifiable'; import { UserSelectedScript } from './UserSelectedScript'; @@ -22,7 +22,7 @@ export class DebouncedScriptSelection implements ScriptSelection { public readonly processChanges: ScriptSelection['processChanges']; constructor( - private readonly collection: ICategoryCollection, + private readonly collection: CategoryCollection, selectedScripts: ReadonlyArray, debounce: DebounceFunction = batchedDebounce, ) { diff --git a/src/application/Context/State/Selection/UserSelectionFacade.ts b/src/application/Context/State/Selection/UserSelectionFacade.ts index 6fbc12bc8..f30982746 100644 --- a/src/application/Context/State/Selection/UserSelectionFacade.ts +++ b/src/application/Context/State/Selection/UserSelectionFacade.ts @@ -1,4 +1,4 @@ -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper'; import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection'; import type { CategorySelection } from './Category/CategorySelection'; @@ -12,7 +12,7 @@ export class UserSelectionFacade implements UserSelection { public readonly scripts: ScriptSelection; constructor( - collection: ICategoryCollection, + collection: CategoryCollection, selectedScripts: readonly SelectedScript[], scriptsFactory = DefaultScriptsFactory, categoriesFactory = DefaultCategoriesFactory, diff --git a/src/application/IApplicationFactory.ts b/src/application/IApplicationFactory.ts deleted file mode 100644 index ed08ab0e4..000000000 --- a/src/application/IApplicationFactory.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { IApplication } from '@/domain/IApplication'; - -export interface IApplicationFactory { - getApp(): Promise; -} diff --git a/src/application/Loader/ApplicationLoader.ts b/src/application/Loader/ApplicationLoader.ts new file mode 100644 index 000000000..098abc81c --- /dev/null +++ b/src/application/Loader/ApplicationLoader.ts @@ -0,0 +1,35 @@ +import type { Application } from '@/domain/Application'; +import { createApplication, type ApplicationFactory } from '@/domain/ApplicationFactory'; +import { loadProjectDetails } from './ProjectDetails/MetadataProjectDetailsLoader'; +import { convertToCategoryCollections, type CompilerAdapter } from './Collections/CompilerAdapter'; +import type { ProjectDetailsLoader } from './ProjectDetails/ProjectDetailsLoader'; +import type { CollectionsLoader } from './Collections/CollectionsLoader'; + +export type ApplicationLoader = ( + utilities?: ApplicationLoadUtilities, +) => Application; + +export function loadApplication( // TODO: Not tested + utilities: ApplicationLoadUtilities = DefaultUtilities, // TODO: Replaces, application parser, move some tests from it +): Application { + const projectDetails = utilities.loadProjectDetails(); + const collections = utilities.loadCollections(projectDetails); + const app = utilities.createApplication({ + projectDetails, + collections, + }); + return app; +} + +interface ApplicationLoadUtilities { + readonly loadProjectDetails: ProjectDetailsLoader; + readonly loadCollections: CollectionsLoader; + readonly createApplication: ApplicationFactory; + readonly convertCollections: CompilerAdapter; +} + +const DefaultUtilities: ApplicationParserUtilities = { + loadProjectDetails: loadProjectDetails, + createApplication: createApplication, + convertCollections: convertToCategoryCollections, +}; diff --git a/src/application/Loader/ApplicationProvider.ts b/src/application/Loader/ApplicationProvider.ts new file mode 100644 index 000000000..30df64718 --- /dev/null +++ b/src/application/Loader/ApplicationProvider.ts @@ -0,0 +1,5 @@ +import type { Application } from '@/domain/Application'; + +export interface ApplicationProvider { // TODO: Use another name than factory? + (): Promise; +} diff --git a/src/application/Loader/Collections/CollectionsLoader.ts b/src/application/Loader/Collections/CollectionsLoader.ts new file mode 100644 index 000000000..f91020ad5 --- /dev/null +++ b/src/application/Loader/Collections/CollectionsLoader.ts @@ -0,0 +1,39 @@ +import { compileCollections, type CollectionsCompiler } from '@/application/Compiler/CollectionCompiler'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import { convertToCategoryCollections, type CompilerAdapter } from './CompilerAdapter'; +import { loadPreloadedCollection } from './DataProvider/PreloadedCollectionDataProvider'; +import type { CollectionDataProvider } from './DataProvider/CollectionDataProvider'; + +export interface CollectionsLoader { + ( + projectDetails: ProjectDetails, + utilities?: CollectionsLoaderUtilities, + ): CategoryCollection[]; +} + +interface CollectionsLoaderUtilities { + readonly compileCollections: CollectionsCompiler; + readonly convertCollection: CompilerAdapter; + readonly loadCollectionFile: CollectionDataProvider; +} + +const DefaultUtilities: CollectionsLoaderUtilities = { + compileCollections, + convertCollection: convertToCategoryCollections, + loadCollectionFile: loadPreloadedCollection, +}; + +export const loadCollections: CollectionsLoader = ( + projectDetails: ProjectDetails, + utilities: CollectionsLoaderUtilities = DefaultUtilities, +) => { + const collectionNames: readonly string[] = ['macos', 'windows', 'linux']; + const collectionsData = collectionNames.map((name) => utilities.loadCollectionFile(name)); + const compiledCollections = utilities.compileCollections( + collectionsData, + projectDetails, + ); + const collections = utilities.convertCollection(compiledCollections); + return collections; +}; diff --git a/src/application/Loader/Collections/CompilerAdapter.ts b/src/application/Loader/Collections/CompilerAdapter.ts new file mode 100644 index 000000000..2548e9099 --- /dev/null +++ b/src/application/Loader/Collections/CompilerAdapter.ts @@ -0,0 +1,83 @@ +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; +import { createCategoryCollection, type CategoryCollectionFactory } from '@/domain/Collection/CategoryCollectionFactory'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; +import { createScriptingDefinition, type ScriptingDefinitionFactory } from '@/domain/ScriptingDefinitionFactory'; +import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory'; +import type { Category } from '@/domain/Executables/Category/Category'; +import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory'; +import type { CompiledCollectionDto } from '../../Compiler/CompiledCollectionDto'; +import type { Script } from '@/domain/Executables/Script/Script'; + +/* +* This module implements the Adapter pattern to convert between the Compiler's +* Data Transfer Objects (DTOs) and the domain model. +* +* It serves as an anti-corruption layer between the compiler and the domain layer, +* allowing each to evolve independently. +*/ + +export interface CompilerAdapter { + ( + compiledCollections: readonly CompiledCollectionDto[], + utilities?: ConverterUtilities, + ): CategoryCollection[]; +} + +interface ConverterUtilities { + readonly createCategoryCollection: CategoryCollectionFactory; + readonly createScriptingDefinition: ScriptingDefinitionFactory; + readonly createCategory: CategoryFactory; + readonly createScript: ScriptFactory; +} + +export const convertToCategoryCollections: CompilerAdapter = ( + collections, + utilities = DefaultUtilities, +) => { + return collections.map( + (collection) => convertToCategoryCollection(collection, utilities), + ); +}; + +const DefaultUtilities: ConverterUtilities = { + createCategoryCollection, + createScriptingDefinition, + createCategory, + createScript, +}; + +function convertToCategoryCollection( + collection: CompiledCollectionDto, + utilities: ConverterUtilities, +): CategoryCollection { + return utilities.createCategoryCollection({ + os: collection.os, + scripting: convertToScriptingDefinition(collection, utilities), + actions: collection.rootCategories.map((category) => convertToCategory(category, utilities)), + }); +} + +function convertToCategory( + category: CompiledCategoryDto, + utilities: ConverterUtilities, +): Category { + +} + +function convertToScript( + script: CompiledScriptDto, + utilities: ConverterUtilities, +): Script { + +} + +function convertToScriptingDefinition( + collection: CompiledCollectionDto, + utilities: ConverterUtilities, +): ScriptingDefinition { + return utilities.createScriptingDefinition({ + language: collection.language, + startCode: collection.startCode, + endCode: collection.endCode, + }); +} diff --git a/src/application/Loader/Collections/DataProvider/CollectionDataProvider.ts b/src/application/Loader/Collections/DataProvider/CollectionDataProvider.ts new file mode 100644 index 000000000..ea3365114 --- /dev/null +++ b/src/application/Loader/Collections/DataProvider/CollectionDataProvider.ts @@ -0,0 +1,5 @@ +import type { CollectionData } from '@/application/collections/'; + +export interface CollectionDataProvider { + (collectionFileName: string): CollectionData; +} diff --git a/src/application/Loader/Collections/DataProvider/PreloadedCollectionDataProvider.ts b/src/application/Loader/Collections/DataProvider/PreloadedCollectionDataProvider.ts new file mode 100644 index 000000000..bd1abc182 --- /dev/null +++ b/src/application/Loader/Collections/DataProvider/PreloadedCollectionDataProvider.ts @@ -0,0 +1,25 @@ +/* YAML data files are loaded at compile time using vite-plugin-yaml` */ +import WindowsData from '@/application/collections/windows.yaml'; +import MacOsData from '@/application/collections/macos.yaml'; +import LinuxData from '@/application/collections/linux.yaml'; +import type { CollectionData } from '@/application/collections/'; +import type { CollectionDataProvider } from './CollectionDataProvider'; + +/** + * Static collection data preloaded at build time. + * This approach improves performance by avoiding runtime file loading. + * The data is compiled into the application bundle as a static asset. + */ +const PreloadedData: Map = new Map([ + ['windows', WindowsData], + ['macos', MacOsData], + ['linux', LinuxData], +]); + +export const loadPreloadedCollection: CollectionDataProvider = (name: string) => { + const data = PreloadedData.get(name); + if (!data) { + throw new Error(`Unknown collection file name "${name}"`); + } + return data; +}; diff --git a/src/application/Loader/LazySingletonApplicationProvider.ts b/src/application/Loader/LazySingletonApplicationProvider.ts new file mode 100644 index 000000000..4ae3836a8 --- /dev/null +++ b/src/application/Loader/LazySingletonApplicationProvider.ts @@ -0,0 +1,19 @@ +import type { Application } from '@/domain/Application'; +import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy'; +import { loadApplication, type ApplicationLoader } from './ApplicationLoader'; +import type { ApplicationProvider } from './ApplicationProvider'; + +let ApplicationGetter: AsyncLazy | undefined; + +type LazySingletonApplicationProvider = ApplicationProvider & (( + loader: ApplicationLoader, +) => ReturnType); + +export const createOrGetApplication: LazySingletonApplicationProvider = ( + loader: ApplicationLoader = loadApplication, +) => { + if (!ApplicationGetter) { + ApplicationGetter = new AsyncLazy(() => Promise.resolve(loader())); + } + return ApplicationGetter.getValue(); +}; diff --git a/src/application/Parser/ProjectDetailsParser.ts b/src/application/Loader/ProjectDetails/MetadataProjectDetailsLoader.ts similarity index 92% rename from src/application/Parser/ProjectDetailsParser.ts rename to src/application/Loader/ProjectDetails/MetadataProjectDetailsLoader.ts index 97749b4e2..28b416956 100644 --- a/src/application/Parser/ProjectDetailsParser.ts +++ b/src/application/Loader/ProjectDetails/MetadataProjectDetailsLoader.ts @@ -6,7 +6,7 @@ import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariabl import type { ConstructorArguments } from '@/TypeHelpers'; export function -parseProjectDetails( +loadProjectDetails( metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance, createProjectDetails: ProjectDetailsFactory = ( ...args @@ -24,10 +24,6 @@ parseProjectDetails( ); } -export interface ProjectDetailsParser { - (): ProjectDetails; -} - export type ProjectDetailsFactory = ( ...args: ConstructorArguments ) => ProjectDetails; diff --git a/src/application/Loader/ProjectDetails/ProjectDetailsLoader.ts b/src/application/Loader/ProjectDetails/ProjectDetailsLoader.ts new file mode 100644 index 000000000..403956505 --- /dev/null +++ b/src/application/Loader/ProjectDetails/ProjectDetailsLoader.ts @@ -0,0 +1,5 @@ +import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; + +export interface ProjectDetailsLoader { + (): ProjectDetails; +} diff --git a/src/application/Parser/ApplicationParser.ts b/src/application/Parser/ApplicationParser.ts deleted file mode 100644 index 27c4c08ec..000000000 --- a/src/application/Parser/ApplicationParser.ts +++ /dev/null @@ -1,48 +0,0 @@ -import type { CollectionData } from '@/application/collections/'; -import type { IApplication } from '@/domain/IApplication'; -import WindowsData from '@/application/collections/windows.yaml'; -import MacOsData from '@/application/collections/macos.yaml'; -import LinuxData from '@/application/collections/linux.yaml'; -import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser'; -import { Application } from '@/domain/Application'; -import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser'; -import { createTypeValidator, type TypeValidator } from './Common/TypeValidator'; - -export function parseApplication( - collectionsData: readonly CollectionData[] = PreParsedCollections, - utilities: ApplicationParserUtilities = DefaultUtilities, -): IApplication { - validateCollectionsData(collectionsData, utilities.validator); - const projectDetails = utilities.parseProjectDetails(); - const collections = collectionsData.map( - (collection) => utilities.parseCategoryCollection(collection, projectDetails), - ); - const app = new Application(projectDetails, collections); - return app; -} - -const PreParsedCollections: readonly CollectionData[] = [ - WindowsData, MacOsData, LinuxData, -]; - -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(), -}; diff --git a/src/application/Parser/CategoryCollectionParser.ts b/src/application/Parser/CategoryCollectionParser.ts index 553f2457e..12a1d9f3e 100644 --- a/src/application/Parser/CategoryCollectionParser.ts +++ b/src/application/Parser/CategoryCollectionParser.ts @@ -1,12 +1,13 @@ import type { CollectionData } from '@/application/collections/'; import { OperatingSystem } from '@/domain/OperatingSystem'; -import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection'; -import { CategoryCollection } from '@/domain/Collection/CategoryCollection'; +import type { CategoryCollection } from '@/domain/Collection/CategoryCollection'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; +import { createCategoryCollection } from '@/domain/Collection/CategoryCollectionFactory'; +import type { CategoryCollectionFactory } from '@/domain/Collection/CategoryCollectionFactory'; import { createEnumParser, type EnumParser } from '../Common/Enum'; +import { createTypeValidator, type TypeValidator } from '../Compiler/Common/TypeValidator'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser'; -import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; -import { createTypeValidator, type TypeValidator } from './Common/TypeValidator'; +import { parseScriptingDefinition, type ScriptingDefinitionCompiler } from '../Compiler/Collection/ScriptingDefinition/ScriptingDefinitionParser'; import { createCategoryCollectionContext, type CategoryCollectionContextFactory } from './Executable/CategoryCollectionContext'; export const parseCategoryCollection: CategoryCollectionParser = ( @@ -14,8 +15,6 @@ export const parseCategoryCollection: CategoryCollectionParser = ( projectDetails, utilities: CategoryCollectionParserUtilities = DefaultUtilities, ) => { - validateCollection(content, utilities.validator); - const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails); const collectionContext = utilities.createContext(content.functions, scripting.language); const categories = content.actions.map( (action) => utilities.parseCategory(action, collectionContext), @@ -27,39 +26,18 @@ export const parseCategoryCollection: CategoryCollectionParser = ( return collection; }; -export type CategoryCollectionFactory = ( - ...parameters: ConstructorParameters -) => ICategoryCollection; - export interface CategoryCollectionParser { ( content: CollectionData, projectDetails: ProjectDetails, utilities?: CategoryCollectionParserUtilities, - ): ICategoryCollection; -} - -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', - }); + ): CategoryCollection; } interface CategoryCollectionParserUtilities { readonly osParser: EnumParser; readonly validator: TypeValidator; - readonly parseScriptingDefinition: ScriptingDefinitionParser; + readonly parseScriptingDefinition: ScriptingDefinitionCompiler; readonly createContext: CategoryCollectionContextFactory; readonly parseCategory: CategoryParser; readonly createCategoryCollection: CategoryCollectionFactory; @@ -71,5 +49,5 @@ const DefaultUtilities: CategoryCollectionParserUtilities = { parseScriptingDefinition, createContext: createCategoryCollectionContext, parseCategory, - createCategoryCollection: (...args) => new CategoryCollection(...args), + createCategoryCollection, }; diff --git a/src/application/Parser/Executable/CategoryParser.ts b/src/application/Parser/Executable/CategoryParser.ts index 80c7e8b8a..bff87196b 100644 --- a/src/application/Parser/Executable/CategoryParser.ts +++ b/src/application/Parser/Executable/CategoryParser.ts @@ -1,7 +1,7 @@ import type { CategoryData, ScriptData, ExecutableData, } from '@/application/collections/'; -import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import type { Category } from '@/domain/Executables/Category/Category'; import type { Script } from '@/domain/Executables/Script/Script'; import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory'; diff --git a/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts b/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts index f3055bb2d..42158ba15 100644 --- a/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Expressions/Parser/Regex/RegexParser.ts @@ -1,4 +1,4 @@ -import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import { Expression, type ExpressionEvaluator } from '../../Expression/Expression'; import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory'; import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory'; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts index b53f5494c..4936b55c8 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument.ts @@ -1,4 +1,4 @@ -import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { createTypeValidator, type TypeValidator } from '@/application/Compiler/Common/TypeValidator'; import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator'; export interface FunctionCallArgument { diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts index 20c77fbb3..0702be271 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/Argument/NestedFunctionArgumentCompiler.ts @@ -5,7 +5,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/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import { createFunctionCallArgument, type FunctionCallArgument, type FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import type { ArgumentCompiler } from './ArgumentCompiler'; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts index e9c9e2a6c..97244d431 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/SingleCall/Strategies/NestedFunctionCallCompiler.ts @@ -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/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import type { ArgumentCompiler } from './Argument/ArgumentCompiler'; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts index 94e56ee58..d874a06b7 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCallsParser.ts @@ -4,7 +4,7 @@ import type { FunctionCallParametersData, } from '@/application/collections/'; import { isArray, isPlainObject } from '@/TypeHelpers'; -import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { createTypeValidator, type TypeValidator } from '@/application/Compiler/Common/TypeValidator'; import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection'; import { ParsedFunctionCall } from './ParsedFunctionCall'; import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument'; diff --git a/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts b/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts index 9577c7c66..0301e7706 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator.ts @@ -1,4 +1,4 @@ -import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator'; +import { createTypeValidator, type TypeValidator } from '@/application/Compiler/Common/TypeValidator'; export interface ParameterNameValidator { ( diff --git a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts index 23f7f05cb..7b77d6505 100644 --- a/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts +++ b/src/application/Parser/Executable/Script/Compiler/Function/SharedFunctionsParser.ts @@ -4,7 +4,7 @@ import type { } from '@/application/collections/'; import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator'; import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; -import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import type { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { CodeValidationRule } from '@/application/Parser/Executable/Script/Validation/CodeValidationRule'; diff --git a/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts b/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts index 1ed34dcfa..eade219f7 100644 --- a/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts +++ b/src/application/Parser/Executable/Script/Compiler/ScriptCompilerFactory.ts @@ -1,7 +1,7 @@ import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator'; -import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/Common/ContextualError'; import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory'; import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings'; import type { ScriptingLanguage } from '@/domain/ScriptingLanguage'; diff --git a/src/application/Parser/Executable/Script/ScriptParser.ts b/src/application/Parser/Executable/Script/ScriptParser.ts index 05e6b8a84..f306129e5 100644 --- a/src/application/Parser/Executable/Script/ScriptParser.ts +++ b/src/application/Parser/Executable/Script/ScriptParser.ts @@ -2,7 +2,7 @@ import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/c import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode'; import { validateCode, type CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator'; -import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError'; +import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Compiler/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'; diff --git a/src/application/Parser/Executable/Validation/ExecutableValidator.ts b/src/application/Parser/Executable/Validation/ExecutableValidator.ts index a5c969014..78c20346a 100644 --- a/src/application/Parser/Executable/Validation/ExecutableValidator.ts +++ b/src/application/Parser/Executable/Validation/ExecutableValidator.ts @@ -1,5 +1,5 @@ import { isString } from '@/TypeHelpers'; -import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator'; +import { createTypeValidator, type TypeValidator } from '../../../Compiler/Common/TypeValidator'; import { type ExecutableErrorContext } from './ExecutableErrorContext'; import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage'; diff --git a/src/domain/Application.ts b/src/domain/Application.ts index 77e85796d..750571335 100644 --- a/src/domain/Application.ts +++ b/src/domain/Application.ts @@ -1,44 +1,11 @@ -import { OperatingSystem } from './OperatingSystem'; -import type { IApplication } from './IApplication'; -import type { ICategoryCollection } from './Collection/ICategoryCollection'; +import type { OperatingSystem } from './OperatingSystem'; +import type { CategoryCollection } from './Collection/CategoryCollection'; import type { ProjectDetails } from './Project/ProjectDetails'; -export class Application implements IApplication { - constructor( - public projectDetails: ProjectDetails, - public collections: readonly ICategoryCollection[], - ) { - validateCollections(collections); - } +export interface Application { + readonly projectDetails: ProjectDetails; + readonly collections: readonly CategoryCollection[]; - public getSupportedOsList(): OperatingSystem[] { - return this.collections.map((collection) => collection.os); - } - - public getCollection(operatingSystem: OperatingSystem): ICategoryCollection { - const collection = this.collections.find((c) => c.os === operatingSystem); - if (!collection) { - throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`); - } - return collection; - } -} - -function validateCollections(collections: readonly ICategoryCollection[]) { - if (!collections.length) { - throw new Error('missing collections'); - } - if (collections.filter((c) => !c).length > 0) { - throw new Error('missing collection in the list'); - } - const osList = collections.map((c) => c.os); - const duplicates = getDuplicates(osList); - if (duplicates.length > 0) { - throw new Error(`multiple collections with same os: ${ - duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`); - } -} - -function getDuplicates(list: readonly OperatingSystem[]): OperatingSystem[] { - return list.filter((os, index) => list.indexOf(os) !== index); + getSupportedOsList(): OperatingSystem[]; + getCollection(operatingSystem: OperatingSystem): CategoryCollection; } diff --git a/src/domain/ApplicationFactory.ts b/src/domain/ApplicationFactory.ts new file mode 100644 index 000000000..954d0d1de --- /dev/null +++ b/src/domain/ApplicationFactory.ts @@ -0,0 +1,59 @@ +import { OperatingSystem } from './OperatingSystem'; +import type { CategoryCollection } from './Collection/CategoryCollection'; +import type { ProjectDetails } from './Project/ProjectDetails'; +import type { Application } from './Application'; + +export interface ApplicationInitParameters { + readonly projectDetails: ProjectDetails; + readonly collections: readonly CategoryCollection[]; +} + +export type ApplicationFactory = ( + parameters: ApplicationInitParameters, +) => Application; + +export const createApplication: ApplicationFactory = (parameters) => { + validateCollections(parameters.collections); + return { + projectDetails: parameters.projectDetails, + collections: parameters.collections, + getSupportedOsList: () => getSupportedOperatingSystems(parameters.collections), + getCollection: (os) => findCollectionByOperatingSystem(os, parameters.collections), + }; +}; + +function findCollectionByOperatingSystem( + os: OperatingSystem, + collections: readonly CategoryCollection[], +) { + const collection = collections.find((c) => c.os === os); + if (!collection) { + throw new Error(`Operating system "${OperatingSystem[os]}" is not defined in application`); + } + return collection; +} + +function getSupportedOperatingSystems( + collections: readonly CategoryCollection[], +): OperatingSystem[] { + return collections.map((collection) => collection.os); +} + +function validateCollections(collections: readonly CategoryCollection[]) { + if (!collections.length) { + throw new Error('missing collections'); + } + if (collections.filter((c) => !c).length > 0) { + throw new Error('missing collection in the list'); + } + const osList = collections.map((c) => c.os); + const duplicates = getDuplicates(osList); + if (duplicates.length > 0) { + throw new Error(`multiple collections with same os: ${ + duplicates.map((os) => OperatingSystem[os].toLowerCase()).join('", "')}`); + } +} + +function getDuplicates(list: readonly OperatingSystem[]): OperatingSystem[] { + return list.filter((os, index) => list.indexOf(os) !== index); +} diff --git a/src/domain/Collection/CategoryCollection.ts b/src/domain/Collection/CategoryCollection.ts index 36f181874..201486bab 100644 --- a/src/domain/Collection/CategoryCollection.ts +++ b/src/domain/Collection/CategoryCollection.ts @@ -1,134 +1,20 @@ -import { getEnumValues, assertInRange } from '@/application/Common/Enum'; -import { RecommendationLevel } from '../Executables/Script/RecommendationLevel'; -import { OperatingSystem } from '../OperatingSystem'; -import { validateCategoryCollection } from './Validation/CompositeCategoryCollectionValidator'; +import type { ScriptingDefinition } from '@/domain/ScriptingDefinition'; +import { OperatingSystem } from '@/domain/OperatingSystem'; +import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel'; +import type { Script } from '@/domain/Executables/Script/Script'; +import type { Category } from '@/domain/Executables/Category/Category'; import type { ExecutableId } from '../Executables/Identifiable'; -import type { Category } from '../Executables/Category/Category'; -import type { Script } from '../Executables/Script/Script'; -import type { IScriptingDefinition } from '../IScriptingDefinition'; -import type { ICategoryCollection } from './ICategoryCollection'; -import type { CategoryCollectionValidator } from './Validation/CategoryCollectionValidator'; -export class CategoryCollection implements ICategoryCollection { - public readonly os: OperatingSystem; - - public readonly actions: ReadonlyArray; - - public readonly scripting: IScriptingDefinition; - - public get totalScripts(): number { return this.queryable.allScripts.length; } - - public get totalCategories(): number { return this.queryable.allCategories.length; } - - private readonly queryable: QueryableCollection; - - constructor( - parameters: CategoryCollectionInitParameters, - validate: CategoryCollectionValidator = validateCategoryCollection, - ) { - this.os = parameters.os; - this.actions = parameters.actions; - this.scripting = parameters.scripting; - - this.queryable = makeQueryable(this.actions); - validate({ - allScripts: this.queryable.allScripts, - allCategories: this.queryable.allCategories, - operatingSystem: this.os, - }); - } - - public getCategory(executableId: ExecutableId): Category { - const category = this.queryable.allCategories.find((c) => c.executableId === executableId); - if (!category) { - throw new Error(`Missing category with ID: "${executableId}"`); - } - return category; - } - - public getScriptsByLevel(level: RecommendationLevel): readonly Script[] { - assertInRange(level, RecommendationLevel); - const scripts = this.queryable.scriptsByLevel.get(level); - return scripts ?? []; - } - - public getScript(executableId: ExecutableId): Script { - const script = this.queryable.allScripts.find((s) => s.executableId === executableId); - if (!script) { - throw new Error(`Missing script: ${executableId}`); - } - return script; - } - - public getAllScripts(): Script[] { - return this.queryable.allScripts; - } - - public getAllCategories(): Category[] { - return this.queryable.allCategories; - } -} - -export interface CategoryCollectionInitParameters { +export interface CategoryCollection { + readonly scripting: ScriptingDefinition; readonly os: OperatingSystem; + readonly totalScripts: number; + readonly totalCategories: number; readonly actions: ReadonlyArray; - readonly scripting: IScriptingDefinition; -} - -interface QueryableCollection { - readonly allCategories: Category[]; - readonly allScripts: Script[]; - readonly scriptsByLevel: Map; -} - -function flattenCategoryHierarchy( - categories: ReadonlyArray, -): [Category[], Script[]] { - const [subCategories, subScripts] = (categories || []) - // Parse children - .map((category) => flattenCategoryHierarchy(category.subcategories)) - // Flatten results - .reduce(([previousCategories, previousScripts], [currentCategories, currentScripts]) => { - return [ - [...previousCategories, ...currentCategories], - [...previousScripts, ...currentScripts], - ]; - }, [new Array(), new Array