refactor to allow switching ICategoryCollection context #40
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
|
||||
export interface IEnvironment {
|
||||
isDesktop: boolean;
|
||||
os: OperatingSystem;
|
||||
readonly isDesktop: boolean;
|
||||
readonly os: OperatingSystem;
|
||||
}
|
||||
|
||||
27
src/application/Parser/ApplicationParser.ts
Normal file
27
src/application/Parser/ApplicationParser.ts
Normal 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;
|
||||
|
||||
@@ -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
47
src/domain/Application.ts
Normal 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);
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
11
src/domain/IApplication.ts
Normal file
11
src/domain/IApplication.ts
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user