refactor extra code, duplicates, complexity

- refactor array equality check and add tests
- remove OperatingSystem.Unknown causing extra logic, return undefined instead
- refactor enum validation to share same logic
- refactor scripting language factories to share same logic
- refactor too many args in runCodeAsync
- refactor ScriptCode constructor to reduce complexity
- fix writing useless write to member object since another property write always override it
This commit is contained in:
undergroundwires
2021-04-11 14:37:02 +01:00
parent 3e9c99f5f8
commit 00d8e551db
37 changed files with 512 additions and 233 deletions

View File

@@ -0,0 +1,21 @@
// Compares to Array<T> objects for equality, ignoring order
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
const sortedArray1 = sort(array1);
const sortedArray2 = sort(array2);
return sequenceEqual(sortedArray1, sortedArray2);
function sort(array: readonly T[]) {
return array.slice().sort();
}
}
// Compares to Array<T> objects for equality in same order
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
if (!array1) { throw new Error('undefined first array'); }
if (!array2) { throw new Error('undefined second array'); }
if (array1.length !== array2.length) {
return false;
}
return array1.every((val, index) => val === array2[index]);
}

View File

@@ -1,6 +1,6 @@
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
type EnumType = number | string;
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
export type EnumType = number | string;
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
export interface IEnumParser<TEnum> {
parseEnum(value: string, propertyName: string): TEnum;
@@ -41,3 +41,14 @@ export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
return getEnumNames(enumVariable)
.map((level) => enumVariable[level]) as TEnumValue[];
}
export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
value: TEnumValue,
enumVariable: EnumVariable<T, TEnumValue>) {
if (value === undefined) {
throw new Error('undefined enum value');
}
if (!(value in enumVariable)) {
throw new RangeError(`enum value "${value}" is out of range`);
}
}

View File

@@ -0,0 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
export interface IScriptingLanguageFactory<T> {
create(language: ScriptingLanguage): T;
}

View File

@@ -0,0 +1,31 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from './IScriptingLanguageFactory';
import { assertInRange } from '@/application/Common/Enum';
type Getter<T> = () => T;
export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageFactory<T> {
private readonly getters = new Map<ScriptingLanguage, Getter<T>>();
public create(language: ScriptingLanguage): T {
assertInRange(language, ScriptingLanguage);
if (!this.getters.has(language)) {
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
const getter = this.getters.get(language);
const instance = getter();
return instance;
}
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
assertInRange(language, ScriptingLanguage);
if (!getter) {
throw new Error('undefined getter');
}
if (this.getters.has(language)) {
throw new Error(`${ScriptingLanguage[language]} is already registered`);
}
this.getters.set(language, getter);
}
}

View File

@@ -5,6 +5,7 @@ import { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { ICategoryCollection } from '@/domain/ICategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum';
type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
@@ -22,7 +23,7 @@ export class ApplicationContext implements IApplicationContext {
public readonly app: IApplication,
initialContext: OperatingSystem) {
validateApp(app);
validateOs(initialContext);
assertInRange(initialContext, OperatingSystem);
this.states = initializeStates(app);
this.changeContext(initialContext);
}
@@ -50,18 +51,6 @@ function validateApp(app: IApplication) {
}
}
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) {

View File

@@ -1,15 +1,14 @@
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
import { BatchBuilder } from './Languages/BatchBuilder';
import { ShellBuilder } from './Languages/ShellBuilder';
import { ICodeBuilderFactory } from './ICodeBuilderFactory';
export class CodeBuilderFactory implements ICodeBuilderFactory {
public create(language: ScriptingLanguage): ICodeBuilder {
switch (language) {
case ScriptingLanguage.shellscript: return new ShellBuilder();
case ScriptingLanguage.batchfile: return new BatchBuilder();
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
export class CodeBuilderFactory extends ScriptingLanguageFactory<ICodeBuilder> implements ICodeBuilderFactory {
constructor() {
super();
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellBuilder());
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchBuilder());
}
}

View File

@@ -1,6 +1,5 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ICodeBuilder } from './ICodeBuilder';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ICodeBuilderFactory {
create(language: ScriptingLanguage): ICodeBuilder;
export interface ICodeBuilderFactory extends IScriptingLanguageFactory<ICodeBuilder> {
}

View File

@@ -4,17 +4,17 @@ import { IBrowserOsDetector } from './IBrowserOsDetector';
export class BrowserOsDetector implements IBrowserOsDetector {
private readonly detectors = BrowserDetectors;
public detect(userAgent: string): OperatingSystem {
public detect(userAgent: string): OperatingSystem | undefined {
if (!userAgent) {
return OperatingSystem.Unknown;
return undefined;
}
for (const detector of this.detectors) {
const os = detector.detect(userAgent);
if (os !== OperatingSystem.Unknown) {
if (os !== undefined) {
return os;
}
}
return OperatingSystem.Unknown;
return undefined;
}
}

View File

@@ -29,10 +29,10 @@ export class DetectorBuilder {
throw new Error('User agent is null or undefined');
}
if (this.existingPartsInUserAgent.some((part) => !userAgent.includes(part))) {
return OperatingSystem.Unknown;
return undefined;
}
if (this.notExistingPartsInUserAgent.some((part) => userAgent.includes(part))) {
return OperatingSystem.Unknown;
return undefined;
}
return this.os;
}

View File

@@ -1,5 +1,5 @@
import { OperatingSystem } from '@/domain/OperatingSystem';
export interface IBrowserOsDetector {
detect(userAgent: string): OperatingSystem;
detect(userAgent: string): OperatingSystem | undefined;
}

View File

@@ -44,7 +44,7 @@ function getProcessPlatform(variables: IEnvironmentVariables): string {
return variables.process.platform;
}
function getDesktopOsType(processPlatform: string): OperatingSystem {
function getDesktopOsType(processPlatform: string): OperatingSystem | undefined {
// https://nodejs.org/api/process.html#process_process_platform
if (processPlatform === 'darwin') {
return OperatingSystem.macOS;
@@ -53,7 +53,7 @@ function getDesktopOsType(processPlatform: string): OperatingSystem {
} else if (processPlatform === 'linux') {
return OperatingSystem.Linux;
}
return OperatingSystem.Unknown;
return undefined;
}
function isDesktop(variables: IEnvironmentVariables): boolean {

View File

@@ -1,9 +1,10 @@
import { ISharedFunction } from './ISharedFunction';
export class SharedFunction implements ISharedFunction {
public readonly parameters: readonly string[];
constructor(
public readonly name: string,
public readonly parameters: readonly string[],
parameters: readonly string[],
public readonly code: string,
public readonly revertCode: string,
) {

View File

@@ -1,6 +1,5 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/IScriptingLanguageFactory';
export interface ISyntaxFactory {
create(language: ScriptingLanguage): ILanguageSyntax;
export interface ISyntaxFactory extends IScriptingLanguageFactory<ILanguageSyntax> {
}

View File

@@ -1,15 +1,14 @@
import { ILanguageSyntax } from '@/domain/ScriptCode';
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ISyntaxFactory } from './ISyntaxFactory';
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax';
import { ISyntaxFactory } from './ISyntaxFactory';
export class SyntaxFactory implements ISyntaxFactory {
public create(language: ScriptingLanguage): ILanguageSyntax {
switch (language) {
case ScriptingLanguage.batchfile: return new BatchFileSyntax();
case ScriptingLanguage.shellscript: return new ShellScriptSyntax();
default: throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
}
export class SyntaxFactory extends ScriptingLanguageFactory<ILanguageSyntax> implements ISyntaxFactory {
constructor() {
super();
this.registerGetter(ScriptingLanguage.batchfile, () => new BatchFileSyntax());
this.registerGetter(ScriptingLanguage.shellscript, () => new ShellScriptSyntax());
}
}

View File

@@ -1,4 +1,4 @@
import { getEnumNames, getEnumValues } from '@/application/Common/Enum';
import { getEnumNames, getEnumValues, assertInRange } from '@/application/Common/Enum';
import { IEntity } from '../infrastructure/Entity/IEntity';
import { ICategory } from './ICategory';
import { IScript } from './IScript';
@@ -21,7 +21,7 @@ export class CategoryCollection implements ICategoryCollection {
throw new Error('undefined scripting definition');
}
this.queryable = makeQueryable(actions);
ensureValidOs(os);
assertInRange(os, OperatingSystem);
ensureValid(this.queryable);
ensureNoDuplicates(this.queryable.allCategories);
ensureNoDuplicates(this.queryable.allScripts);
@@ -54,18 +54,6 @@ export class CategoryCollection implements ICategoryCollection {
}
}
function ensureValidOs(os: OperatingSystem): void {
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 ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
const totalOccurrencesById = new Map<TKey, number>();
for (const entity of entities) {

View File

@@ -10,5 +10,4 @@ export enum OperatingSystem {
Android,
iOS,
WindowsPhone,
Unknown,
}

View File

@@ -1,5 +1,6 @@
import { IProjectInformation } from './IProjectInformation';
import { OperatingSystem } from './OperatingSystem';
import { assertInRange } from '@/application/Common/Enum';
export class ProjectInformation implements IProjectInformation {
public readonly repositoryWebUrl: string;
@@ -42,6 +43,7 @@ function getWebUrl(gitUrl: string) {
}
function getFileName(os: OperatingSystem, version: string): string {
assertInRange(os, OperatingSystem);
switch (os) {
case OperatingSystem.Linux:
return `privacy.sexy-${version}.AppImage`;
@@ -50,6 +52,6 @@ function getFileName(os: OperatingSystem, version: string): string {
case OperatingSystem.Windows:
return `privacy.sexy-Setup-${version}.exe`;
default:
throw new Error(`Unsupported os: ${OperatingSystem[os]}`);
throw new RangeError(`Unsupported os: ${OperatingSystem[os]}`);
}
}

View File

@@ -7,16 +7,7 @@ export class ScriptCode implements IScriptCode {
syntax: ILanguageSyntax) {
if (!syntax) { throw new Error('undefined syntax'); }
validateCode(execute, syntax);
if (revert) {
try {
validateCode(revert, syntax);
if (execute === revert) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
validateRevertCode(revert, execute, syntax);
}
}
@@ -25,6 +16,20 @@ export interface ILanguageSyntax {
readonly commonCodeParts: string[];
}
function validateRevertCode(revertCode: string, execute: string, syntax: ILanguageSyntax) {
if (!revertCode) {
return;
}
try {
validateCode(revertCode, syntax);
if (execute === revertCode) {
throw new Error(`Code itself and its reverting code cannot be the same`);
}
} catch (err) {
throw Error(`(revert): ${err.message}`);
}
}
function validateCode(code: string, syntax: ILanguageSyntax): void {
if (!code || code.length === 0) {
throw new Error(`code is empty or undefined`);

View File

@@ -5,16 +5,20 @@ import fs from 'fs';
import child_process from 'child_process';
import { OperatingSystem } from '@/domain/OperatingSystem';
export async function runCodeAsync(
code: string, folderName: string, fileExtension: string,
node = getNodeJs(), environment = Environment.CurrentEnvironment): Promise<void> {
const dir = node.path.join(node.os.tmpdir(), folderName);
await node.fs.promises.mkdir(dir, {recursive: true});
const filePath = node.path.join(dir, `run.${fileExtension}`);
await node.fs.promises.writeFile(filePath, code);
await node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, environment);
node.child_process.exec(command);
export class CodeRunner {
constructor(
private readonly node = getNodeJs(),
private readonly environment = Environment.CurrentEnvironment) {
}
public async runCodeAsync(code: string, folderName: string, fileExtension: string): Promise<void> {
const dir = this.node.path.join(this.node.os.tmpdir(), folderName);
await this.node.fs.promises.mkdir(dir, {recursive: true});
const filePath = this.node.path.join(dir, `run.${fileExtension}`);
await this.node.fs.promises.writeFile(filePath, code);
await this.node.fs.promises.chmod(filePath, '755');
const command = getExecuteCommand(filePath, this.environment);
this.node.child_process.exec(command);
}
}
function getExecuteCommand(scriptPath: string, environment: Environment): string {

View File

@@ -37,7 +37,7 @@ import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IApplicationCode } from '@/application/Context/State/Code/IApplicationCode';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { runCodeAsync } from '@/infrastructure/CodeRunner';
import { CodeRunner } from '@/infrastructure/CodeRunner';
import { IApplicationContext } from '@/application/Context/IApplicationContext';
@Component({
@@ -116,11 +116,12 @@ function buildFileName(scripting: IScriptingDefinition) {
}
async function executeCodeAsync(context: IApplicationContext) {
await runCodeAsync(
/*code*/ context.state.code.current,
/*appName*/ context.app.info.name,
/*fileExtension*/ context.state.collection.scripting.fileExtension,
);
const runner = new CodeRunner();
await runner.runCodeAsync(
/*code*/ context.state.code.current,
/*appName*/ context.app.info.name,
/*fileExtension*/ context.state.collection.scripting.fileExtension,
);
}
</script>

View File

@@ -24,7 +24,7 @@ import { ApplicationFactory } from '@/application/ApplicationFactory';
@Component
export default class TheOsChanger extends StatefulVue {
public allOses: Array<{ name: string, os: OperatingSystem }> = [];
public currentOs: OperatingSystem = OperatingSystem.Unknown;
public currentOs?: OperatingSystem = undefined;
public async created() {
const app = await ApplicationFactory.Current.getAppAsync();