refactor to allow switching ICategoryCollection context #40

This commit is contained in:
undergroundwires
2021-01-05 22:28:38 +01:00
parent 3455a2ca6c
commit 2e40605d59
32 changed files with 897 additions and 232 deletions

View File

@@ -1,20 +1,72 @@
import { IApplicationContext } from './IApplicationContext';
import { IApplicationContext, IApplicationContextChangedEvent } from './IApplicationContext';
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { CategoryCollectionState } from './State/CategoryCollectionState';
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
import { parseCategoryCollection } from '../Parser/CategoryCollectionParser';
import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { Signal } from '@/infrastructure/Events/Signal';
export function createContext(): IApplicationContext {
const application = parseCategoryCollection(applicationFile);
const context = new ApplicationContext(application);
return context;
}
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext {
public readonly state: ICategoryCollectionState;
public constructor(public readonly collection: ICategoryCollection) {
this.state = new CategoryCollectionState(collection);
public readonly contextChanged = new Signal<IApplicationContextChangedEvent>();
public collection: ICategoryCollection;
public currentOs: OperatingSystem;
public get state(): ICategoryCollectionState {
return this.states[this.collection.os];
}
private readonly states: StateMachine;
public constructor(
public readonly app: IApplication,
initialContext: OperatingSystem) {
validateApp(app);
validateOs(initialContext);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
public changeContext(os: OperatingSystem): void {
if (this.currentOs === os) {
return;
}
this.collection = this.app.getCollection(os);
if (!this.collection) {
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
}
const event: IApplicationContextChangedEvent = {
newState: this.state,
newCollection: this.collection,
newOs: os,
};
this.contextChanged.notify(event);
this.currentOs = os;
}
}
function validateApp(app: IApplication) {
if (!app) {
throw new Error('undefined app');
}
}
function validateOs(os: OperatingSystem) {
if (os === undefined) {
throw new Error('undefined os');
}
if (os === OperatingSystem.Unknown) {
throw new Error('unknown os');
}
if (!(os in OperatingSystem)) {
throw new Error(`os "${os}" is out of range`);
}
}
function initializeStates(app: IApplication): StateMachine {
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
for (const collection of app.collections) {
machine[collection.os] = new CategoryCollectionState(collection);
}
return machine;
}

View File

@@ -1,9 +1,32 @@
import { ApplicationContext } from './ApplicationContext';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
import { parseCategoryCollection } from '@/application/Parser/CategoryCollectionParser';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { Environment } from '../Environment/Environment';
import { IApplication } from '@/domain/IApplication';
import { IEnvironment } from '../Environment/IEnvironment';
import { parseApplication } from '../Parser/ApplicationParser';
export function buildContext(): IApplicationContext {
const application = parseCategoryCollection(applicationFile);
return new ApplicationContext(application);
export type ApplicationParserType = () => IApplication;
const ApplicationParser: ApplicationParserType = parseApplication;
export function buildContext(
parser = ApplicationParser,
environment = Environment.CurrentEnvironment): IApplicationContext {
const app = parser();
const os = getInitialOs(app, environment);
return new ApplicationContext(app, os);
}
function getInitialOs(app: IApplication, environment: IEnvironment): OperatingSystem {
const currentOs = environment.os;
const supportedOsList = app.getSupportedOsList();
if (supportedOsList.includes(currentOs)) {
return currentOs;
}
supportedOsList.sort((os1, os2) => {
const os1SupportLevel = app.collections[os1].totalScripts;
const os2SupportLevel = app.collections[os2].totalScripts;
return os1SupportLevel - os2SupportLevel;
});
return supportedOsList[0];
}

View File

@@ -1,7 +1,20 @@
import { ICategoryCollectionState } from './State/ICategoryCollectionState';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ISignal } from '@/infrastructure/Events/ISignal';
import { IApplication } from '@/domain/IApplication';
export interface IApplicationContext {
readonly currentOs: OperatingSystem;
readonly app: IApplication;
readonly collection: ICategoryCollection;
readonly state: ICategoryCollectionState;
readonly contextChanged: ISignal<IApplicationContextChangedEvent>;
changeContext(os: OperatingSystem): void;
}
export interface IApplicationContextChangedEvent {
readonly newState: ICategoryCollectionState;
readonly newCollection: ICategoryCollection;
readonly newOs: OperatingSystem;
}

View File

@@ -1,6 +1,6 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IEnvironment {
isDesktop: boolean;
os: OperatingSystem;
readonly isDesktop: boolean;
readonly os: OperatingSystem;
}

View File

@@ -0,0 +1,27 @@
import { IApplication } from '@/domain/IApplication';
import { IProjectInformation } from '@/domain/IProjectInformation';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { parseCategoryCollection } from './CategoryCollectionParser';
import applicationFile, { YamlApplication } from 'js-yaml-loader!@/application/application.yaml';
import { parseProjectInformation } from '@/application/Parser/ProjectInformationParser';
import { Application } from '@/domain/Application';
export function parseApplication(
parser = CategoryCollectionParser,
processEnv: NodeJS.ProcessEnv = process.env,
collectionData = CollectionData): IApplication {
const information = parseProjectInformation(processEnv);
const collection = parser(collectionData, information);
const app = new Application(information, [ collection ]);
return app;
}
export type CategoryCollectionParserType
= (file: YamlApplication, info: IProjectInformation) => ICategoryCollection;
const CategoryCollectionParser: CategoryCollectionParserType
= (file, info) => parseCategoryCollection(file, info);
const CollectionData: YamlApplication
= applicationFile;

View File

@@ -1,17 +1,17 @@
import { Category } from '@/domain/Category';
import { YamlApplication } from 'js-yaml-loader!@/application.yaml';
import { parseCategory } from './CategoryParser';
import { parseProjectInformation } from './ProjectInformationParser';
import { ScriptCompiler } from './Compiler/ScriptCompiler';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
import { createEnumParser } from '../Common/Enum';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection';
import { IProjectInformation } from '@/domain/IProjectInformation';
export function parseCategoryCollection(
content: YamlApplication,
env: NodeJS.ProcessEnv = process.env,
info: IProjectInformation,
osParser = createEnumParser(OperatingSystem)): ICategoryCollection {
validate(content);
const compiler = new ScriptCompiler(content.functions);
@@ -21,11 +21,9 @@ export function parseCategoryCollection(
categories.push(category);
}
const os = osParser.parseEnum(content.os, 'os');
const info = parseProjectInformation(env);
const scripting = parseScriptingDefinition(content.scripting, info);
const collection = new CategoryCollection(
os,
info,
categories,
scripting);
return collection;

47
src/domain/Application.ts Normal file
View File

@@ -0,0 +1,47 @@
import { IApplication } from './IApplication';
import { ICategoryCollection } from './ICategoryCollection';
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
export class Application implements IApplication {
constructor(public info: IProjectInformation, public collections: readonly ICategoryCollection[]) {
validateInformation(info);
validateCollections(collections);
}
public getSupportedOsList(): OperatingSystem[] {
return this.collections.map((collection) => collection.os);
}
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
return this.collections.find((collection) => collection.os === operatingSystem);
}
}
function validateInformation(info: IProjectInformation) {
if (!info) {
throw new Error('undefined project information');
}
}
function validateCollections(collections: readonly ICategoryCollection[]) {
if (!collections) {
throw new Error('undefined collections');
}
if (collections.length === 0) {
throw new Error('no collection in the list');
}
if (collections.filter((c) => !c).length > 0) {
throw new Error('undefined 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);
}

View File

@@ -2,7 +2,6 @@ import { getEnumNames, getEnumValues } from '@/application/Common/Enum';
import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory';
import { IScript } from './IScript';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel } from './RecommendationLevel';
import { OperatingSystem } from './OperatingSystem';
import { IScriptingDefinition } from './IScriptingDefinition';
@@ -16,12 +15,8 @@ export class CategoryCollection implements ICategoryCollection {
constructor(
public readonly os: OperatingSystem,
public readonly info: IProjectInformation,
public readonly actions: ReadonlyArray<ICategory>,
public readonly scripting: IScriptingDefinition) {
if (!info) {
throw new Error('undefined info');
}
if (!scripting) {
throw new Error('undefined scripting definition');
}

View File

@@ -0,0 +1,11 @@
import { ICategoryCollection } from './ICategoryCollection';
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
export interface IApplication {
readonly info: IProjectInformation;
readonly collections: readonly ICategoryCollection[];
getSupportedOsList(): OperatingSystem[];
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
}

View File

@@ -3,10 +3,8 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import { RecommendationLevel } from '@/domain/RecommendationLevel';
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IProjectInformation } from '@/domain/IProjectInformation';
export interface ICategoryCollection {
readonly info: IProjectInformation;
readonly scripting: IScriptingDefinition;
readonly os: OperatingSystem;
readonly totalScripts: number;

View File

@@ -74,7 +74,7 @@
public async mounted() {
const context = await this.getCurrentContextAsync();
this.repositoryUrl = context.collection.info.repositoryWebUrl;
this.repositoryUrl = context.app.info.repositoryWebUrl;
const filter = context.state.filter;
filter.filterRemoved.on(() => {
this.isSearching = false;

View File

@@ -39,7 +39,7 @@ export default class DownloadUrlListItem extends StatefulVue {
private async getDownloadUrlAsync(os: OperatingSystem): Promise<string> {
const context = await this.getCurrentContextAsync();
return context.collection.info.getDownloadUrl(os);
return context.app.info.getDownloadUrl(os);
}
}

View File

@@ -48,8 +48,9 @@ export default class TheFooter extends StatefulVue {
public async mounted() {
const context = await this.getCurrentContextAsync();
this.repositoryUrl = context.collection.info.repositoryWebUrl;
this.feedbackUrl = context.collection.info.feedbackUrl;
const info = context.app.info;
this.repositoryUrl = info.repositoryWebUrl;
this.feedbackUrl = info.feedbackUrl;
}
}
</script>

View File

@@ -75,7 +75,7 @@ export default class TheFooter extends StatefulVue {
public async mounted() {
const context = await this.getCurrentContextAsync();
const info = context.collection.info;
const info = context.app.info;
this.version = info.version;
this.homepageUrl = info.homepage;
this.repositoryUrl = info.repositoryWebUrl;

View File

@@ -16,7 +16,7 @@ export default class TheHeader extends StatefulVue {
public async mounted() {
const context = await this.getCurrentContextAsync();
this.title = context.collection.info.name;
this.title = context.app.info.name;
}
}
</script>