Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d328f08952 | ||
|
|
bcad357017 | ||
|
|
9845a7cd68 | ||
|
|
7c632f7388 | ||
|
|
1442f62633 | ||
|
|
7f7a84e3ba | ||
|
|
dee3279f85 | ||
|
|
094dbb01b8 | ||
|
|
e299d40fa1 | ||
|
|
cb42f11b97 | ||
|
|
4531645b4c | ||
|
|
bf3426f91b | ||
|
|
3864f04218 | ||
|
|
e541a35e86 | ||
|
|
bd383ed273 | ||
|
|
949fac1a7c | ||
|
|
7ab16ecccb | ||
|
|
58cd551a30 | ||
|
|
7770a9b521 | ||
|
|
aab0f7ea46 | ||
|
|
ea41f4f503 |
@@ -10,7 +10,7 @@ module.exports = {
|
|||||||
},
|
},
|
||||||
extends: [
|
extends: [
|
||||||
// Vue specific rules, eslint-plugin-vue
|
// Vue specific rules, eslint-plugin-vue
|
||||||
'plugin:vue/essential',
|
'plugin:vue/vue3-recommended',
|
||||||
|
|
||||||
// Extends eslint-config-airbnb
|
// Extends eslint-config-airbnb
|
||||||
'@vue/eslint-config-airbnb-with-typescript',
|
'@vue/eslint-config-airbnb-with-typescript',
|
||||||
|
|||||||
38
.github/workflows/tests.e2e.yaml
vendored
38
.github/workflows/tests.e2e.yaml
vendored
@@ -24,3 +24,41 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Run e2e tests
|
name: Run e2e tests
|
||||||
run: npm run test:cy:run
|
run: npm run test:cy:run
|
||||||
|
-
|
||||||
|
name: Output artifact directories
|
||||||
|
id: artifacts
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
shell: bash
|
||||||
|
run: |-
|
||||||
|
declare -r dirs_json_file='cypress-dirs.json'
|
||||||
|
if [ ! -f "${dirs_json_file}" ]; then
|
||||||
|
echo "${dirs_json_file} does not exist"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
SCREENSHOTS_DIR=$(jq -r '.screenshots' "${dirs_json_file}")
|
||||||
|
VIDEOS_DIR=$(jq -r '.videos' "${dirs_json_file}")
|
||||||
|
|
||||||
|
for dir in "${SCREENSHOTS_DIR}" "${VIDEOS_DIR}"; do
|
||||||
|
if [ "${dir}" = 'null' ] || [ -z "${dir}" ]; then
|
||||||
|
echo "One or more directories are null or not specified in cypress-dirs.json"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
echo "SCREENSHOTS_DIR=${SCREENSHOTS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
echo "VIDEOS_DIR=${VIDEOS_DIR}" >> "${GITHUB_OUTPUT}"
|
||||||
|
-
|
||||||
|
name: Upload screenshots
|
||||||
|
if: failure() # Run only if previous steps fail because screenshots will be generated only if E2E test failed
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-screenshots-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.SCREENSHOTS_DIR }}
|
||||||
|
-
|
||||||
|
name: Upload videos
|
||||||
|
if: always() # Run even if previous steps fail because test run video is always captured
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: e2e-videos-${{ matrix.os }}
|
||||||
|
path: ${{ steps.artifacts.outputs.VIDEOS_DIR }}
|
||||||
|
|||||||
14
.gitignore
vendored
14
.gitignore
vendored
@@ -1,5 +1,13 @@
|
|||||||
node_modules
|
# Application build artifacts
|
||||||
/dist-*/
|
/dist-*/
|
||||||
.vs
|
|
||||||
|
# npm
|
||||||
|
node_modules
|
||||||
|
|
||||||
|
# Visual Studio Code
|
||||||
.vscode/**/*
|
.vscode/**/*
|
||||||
!.vscode/extensions.json
|
!.vscode/extensions.json
|
||||||
|
|
||||||
|
# draw.io
|
||||||
|
*.bkp
|
||||||
|
*.dtmp
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## 0.12.7 (2023-11-07)
|
||||||
|
|
||||||
|
* Add winget download instructions | [b2ffc90](https://github.com/undergroundwires/privacy.sexy/commit/b2ffc90da70367b9e65c82556e8f440f865ceb98)
|
||||||
|
* Fix unresponsive copy button on instructions modal | [8ccaec7](https://github.com/undergroundwires/privacy.sexy/commit/8ccaec7af6ea3ecfd46bab5c13b90f71d55e32c1)
|
||||||
|
* Fix tree node check states not being updated | [af7219f](https://github.com/undergroundwires/privacy.sexy/commit/af7219f6e12ab4a65ce07190f691cf3234e87e35)
|
||||||
|
|
||||||
|
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.12.6...0.12.7)
|
||||||
|
|
||||||
## 0.12.6 (2023-11-03)
|
## 0.12.6 (2023-11-03)
|
||||||
|
|
||||||
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
* Bump dependencies to latest | [25d7f7b](https://github.com/undergroundwires/privacy.sexy/commit/25d7f7b2a479e51e092881cc2751e67a7d3f179f)
|
||||||
|
|||||||
@@ -122,7 +122,7 @@
|
|||||||
## Get started
|
## Get started
|
||||||
|
|
||||||
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
- 🌍️ **Online**: [https://privacy.sexy](https://privacy.sexy).
|
||||||
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-Setup-0.12.6.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.6/privacy.sexy-0.12.6.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-Setup-0.12.7.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.12.7/privacy.sexy-0.12.7.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
Online version does not require to run any software on your computer. Offline version has more functions such as running the scripts directly.
|
||||||
|
|
||||||
|
|||||||
5
cypress-dirs.json
Normal file
5
cypress-dirs.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"base": "tests/e2e",
|
||||||
|
"videos": "tests/e2e/videos",
|
||||||
|
"screenshots": "tests/e2e/videos"
|
||||||
|
}
|
||||||
@@ -1,18 +1,31 @@
|
|||||||
import { defineConfig } from 'cypress';
|
import { defineConfig } from 'cypress';
|
||||||
import ViteConfig from './vite.config';
|
import ViteConfig from './vite.config';
|
||||||
|
import cypressDirs from './cypress-dirs.json' assert { type: 'json' };
|
||||||
const CYPRESS_BASE_DIR = 'tests/e2e/';
|
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
fixturesFolder: `${CYPRESS_BASE_DIR}/fixtures`,
|
fixturesFolder: `${cypressDirs.base}/fixtures`,
|
||||||
screenshotsFolder: `${CYPRESS_BASE_DIR}/screenshots`,
|
screenshotsFolder: cypressDirs.screenshots,
|
||||||
|
|
||||||
video: true,
|
video: true,
|
||||||
videosFolder: `${CYPRESS_BASE_DIR}/videos`,
|
videosFolder: cypressDirs.videos,
|
||||||
|
|
||||||
e2e: {
|
e2e: {
|
||||||
baseUrl: `http://localhost:${ViteConfig.server.port}/`,
|
baseUrl: `http://localhost:${getApplicationPort()}/`,
|
||||||
specPattern: `${CYPRESS_BASE_DIR}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
specPattern: `${cypressDirs.base}/**/*.cy.{js,jsx,ts,tsx}`, // Default: cypress/e2e/**/*.cy.{js,jsx,ts,tsx}
|
||||||
supportFile: `${CYPRESS_BASE_DIR}/support/e2e.ts`,
|
supportFile: `${cypressDirs.base}/support/e2e.ts`,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/*
|
||||||
|
Disabling Chrome's web security to allow for faster DOM queries to access DOM earlier than
|
||||||
|
`cy.get()`. It bypasses the usual same-origin policy constraints
|
||||||
|
*/
|
||||||
|
chromeWebSecurity: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function getApplicationPort(): number {
|
||||||
|
const port = ViteConfig.server?.port;
|
||||||
|
if (port === undefined) {
|
||||||
|
throw new Error('Unknown application port');
|
||||||
|
}
|
||||||
|
return port;
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
## Structure
|
## Structure
|
||||||
|
|
||||||
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
- [`/src/` **`presentation/`**](./../src/presentation/): Contains Vue and Electron code.
|
||||||
|
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
||||||
|
- [**`index.html`**](./../src/presentation/index.html): The `index.html` entry file, located at the root of the project as required by Vite
|
||||||
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
- [**`bootstrapping/`**](./../src/presentation/bootstrapping/): Registers Vue components and plugins.
|
||||||
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
- [**`components/`**](./../src/presentation/components/): Contains Vue components and helpers.
|
||||||
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
- [**`Shared/`**](./../src/presentation/components/Shared): Contains shared Vue components and helpers.
|
||||||
@@ -20,8 +22,7 @@ The presentation layer uses an event-driven architecture for bidirectional react
|
|||||||
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
- [**`fonts/`**](./../src/presentation/assets/fonts/): Contains fonts.
|
||||||
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
- [**`styles/`**](./../src/presentation/assets/styles/): Contains shared styles.
|
||||||
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
- [**`components/`**](./../src/presentation/assets/styles/components): Contains styles coupled to Vue components.
|
||||||
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint.
|
- [**`main.scss`**](./../src/presentation/assets/styles/main.scss): Main Sass file, imported by other components as single entrypoint..
|
||||||
- [**`main.ts`**](./../src/presentation/main.ts): Starts Vue app.
|
|
||||||
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
- [**`electron/`**](./../src/presentation/electron/): Contains Electron code.
|
||||||
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
- [`/main/` **`index.ts`**](./../src/presentation/main.ts): Main entry for Electron, managing application windows and lifecycle events.
|
||||||
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
- [`/preload/` **`index.ts`**](./../src/presentation/main.ts): Script executed before the renderer, securing Node.js features for renderer use.
|
||||||
@@ -70,10 +71,11 @@ To add a new dependency:
|
|||||||
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
1. **Define its symbol**: Define an associated symbol for every dependency in [`injectionSymbols.ts`](./../src/presentation/injectionSymbols.ts). Symbols are grouped into:
|
||||||
- **Singletons**: Shared across components, instantiated once.
|
- **Singletons**: Shared across components, instantiated once.
|
||||||
- **Transients**: Factories yielding a new instance on every access.
|
- **Transients**: Factories yielding a new instance on every access.
|
||||||
2. **Provide the dependency**: Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency. [`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
2. **Provide the dependency**:
|
||||||
3. **Inject the dependency**: Use Vue's `inject` method alongside the defined symbol to incorporate the dependency into components.
|
Modify the [`provideDependencies`](./../src/presentation/bootstrapping/DependencyProvider.ts) function to include the new dependency.
|
||||||
- For singletons, invoke the factory method: `inject(symbolKey)()`.
|
[`App.vue`](./../src/presentation/components/App.vue) calls this function within its `setup()` hook to register the dependencies.
|
||||||
- For transients, directly inject: `inject(symbolKey)`.
|
3. **Inject the dependency**: Use `injectKey` to inject a dependency. Pass a selector function to `injectKey` that retrieves the appropriate symbol from the provided dependencies.
|
||||||
|
- Example usage: `injectKey((keys) => keys.useCollectionState)`;
|
||||||
|
|
||||||
## Shared UI components
|
## Shared UI components
|
||||||
|
|
||||||
|
|||||||
@@ -68,21 +68,23 @@ These checks validate various qualities like runtime execution, building process
|
|||||||
- [`./src/`](./../src/): Contains the code subject to testing.
|
- [`./src/`](./../src/): Contains the code subject to testing.
|
||||||
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
- [`./tests/shared/`](./../tests/shared/): Contains code shared by different test categories.
|
||||||
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
- [`bootstrap/setup.ts`](./../tests/shared/bootstrap/setup.ts): Initializes unit and integration tests.
|
||||||
|
- [`Assertions/`](./../tests/shared/Assertions/): Contains common assertion functions, prefixed with `expect`.
|
||||||
- [`./tests/unit/`](./../tests/unit/)
|
- [`./tests/unit/`](./../tests/unit/)
|
||||||
- Stores unit test code.
|
- Stores unit test code.
|
||||||
- The directory structure mirrors [`./src/`](./../src).
|
- The directory structure mirrors [`./src/`](./../src).
|
||||||
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
- E.g., tests for [`./src/application/ApplicationFactory.ts`](./../src/application/ApplicationFactory.ts) reside in [`./tests/unit/application/ApplicationFactory.spec.ts`](./../tests/unit/application/ApplicationFactory.spec.ts).
|
||||||
- [`shared/`](./../tests/unit/shared/)
|
- [`shared/`](./../tests/unit/shared/)
|
||||||
- Contains shared unit test functionalities.
|
- Contains shared unit test functionalities.
|
||||||
- [`Assertions/`](./../tests/unit/shared/Assertions): Contains common assertion functions, prefixed with `expect`.
|
|
||||||
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
- [`TestCases/`](./../tests/unit/shared/TestCases/)
|
||||||
- Shared test cases.
|
- Shared test cases.
|
||||||
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
- Functions that calls `it()` from [Vitest](https://vitest.dev/) should have `it` prefix.
|
||||||
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
- [`Stubs/`](./../tests/unit/shared/Stubs): Maintains stubs for component isolation, equipped with basic functionalities and, when necessary, spying or mocking capabilities.
|
||||||
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
- [`./tests/integration/`](./../tests/integration/): Contains integration test files.
|
||||||
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
- [`cypress.config.ts`](./../cypress.config.ts): Cypress (E2E tests) configuration file.
|
||||||
|
- [`cypress-dirs.json`](./../cypress-dirs.json): A central definition of directories used by Cypress, designed for reuse across different configurations.
|
||||||
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
- [`./tests/e2e/`](./../tests/e2e/): Base Cypress folder, includes tests with `.cy.ts` extension.
|
||||||
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single spec file.
|
|
||||||
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
- [`/tsconfig.json`]: TypeScript configuration for file Cypress code, improves IDE support, recommended to have by official documentation.
|
||||||
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
- *(git ignored)* `/videos`: Asset folder for videos taken during tests.
|
||||||
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
- *(git ignored)* `/screenshots`: Asset folder for Screenshots taken during tests.
|
||||||
|
- [`/support/e2e.ts`](./../tests/e2e/support/e2e.ts): Support file, runs before every single test file.
|
||||||
|
- [`/support/interactions/`](./../tests/e2e/support/interactions/): Contains reusable functions for simulating user interactions, enhancing test readability and maintainability.
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.6",
|
"version": "0.12.7",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.5",
|
"version": "0.12.7",
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.0.2",
|
"@floating-ui/vue": "^1.0.2",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.12.6",
|
"version": "0.12.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Now you have the choice",
|
"slogan": "Now you have the choice",
|
||||||
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
"description": "Enforce privacy & security best-practices on Windows, macOS and Linux, because privacy is sexy 🍑🍆",
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
"electron:preview": "electron-vite preview",
|
"electron:preview": "electron-vite preview",
|
||||||
"electron:prebuild": "electron-vite build",
|
"electron:prebuild": "electron-vite build",
|
||||||
"electron:build": "electron-builder",
|
"electron:build": "electron-builder",
|
||||||
"lint:eslint": "eslint . --ignore-path .gitignore",
|
"lint:eslint": "eslint . --max-warnings=0 --ignore-path .gitignore",
|
||||||
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
"lint:md": "markdownlint **/*.md --ignore node_modules",
|
||||||
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
"lint:md:consistency": "remark . --frail --use remark-preset-lint-consistent",
|
||||||
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
"lint:md:relative-urls": "remark . --frail --use remark-validate-links",
|
||||||
|
|||||||
@@ -12,9 +12,6 @@ export class ApplicationFactory implements IApplicationFactory {
|
|||||||
private readonly getter: AsyncLazy<IApplication>;
|
private readonly getter: AsyncLazy<IApplication>;
|
||||||
|
|
||||||
protected constructor(costlyGetter: ApplicationGetterType) {
|
protected constructor(costlyGetter: ApplicationGetterType) {
|
||||||
if (!costlyGetter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
this.getter = new AsyncLazy<IApplication>(() => Promise.resolve(costlyGetter()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
// Compares to Array<T> objects for equality, ignoring order
|
// Compares to Array<T> objects for equality, ignoring order
|
||||||
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
const sortedArray1 = sort(array1);
|
const sortedArray1 = sort(array1);
|
||||||
const sortedArray2 = sort(array2);
|
const sortedArray2 = sort(array2);
|
||||||
return sequenceEqual(sortedArray1, sortedArray2);
|
return sequenceEqual(sortedArray1, sortedArray2);
|
||||||
@@ -12,8 +10,6 @@ export function scrambledEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
|||||||
|
|
||||||
// Compares to Array<T> objects for equality in same order
|
// Compares to Array<T> objects for equality in same order
|
||||||
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
export function sequenceEqual<T>(array1: readonly T[], array2: readonly T[]) {
|
||||||
if (!array1) { throw new Error('missing first array'); }
|
|
||||||
if (!array2) { throw new Error('missing second array'); }
|
|
||||||
if (array1.length !== array2.length) {
|
if (array1.length !== array2.length) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,23 +20,30 @@ export abstract class CustomError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Environment = {
|
interface ErrorPrototypeManipulation {
|
||||||
|
getSetPrototypeOf: () => (typeof Object.setPrototypeOf | undefined);
|
||||||
|
getCaptureStackTrace: () => (typeof Error.captureStackTrace | undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PlatformErrorPrototypeManipulation: ErrorPrototypeManipulation = {
|
||||||
getSetPrototypeOf: () => Object.setPrototypeOf,
|
getSetPrototypeOf: () => Object.setPrototypeOf,
|
||||||
getCaptureStackTrace: () => Error.captureStackTrace,
|
getCaptureStackTrace: () => Error.captureStackTrace,
|
||||||
};
|
};
|
||||||
|
|
||||||
function fixPrototype(target: Error, prototype: CustomError) {
|
function fixPrototype(target: Error, prototype: CustomError) {
|
||||||
// https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
// This is recommended by TypeScript guidelines.
|
||||||
const setPrototypeOf = Environment.getSetPrototypeOf();
|
// Source: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget
|
||||||
if (!functionExists(setPrototypeOf)) {
|
// Snapshots: https://web.archive.org/web/20231111234849/https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-2.html#support-for-newtarget, https://archive.ph/tr7cX#support-for-newtarget
|
||||||
|
const setPrototypeOf = PlatformErrorPrototypeManipulation.getSetPrototypeOf();
|
||||||
|
if (!isFunction(setPrototypeOf)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setPrototypeOf(target, prototype);
|
setPrototypeOf(target, prototype);
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureStackTrace(target: Error) {
|
function ensureStackTrace(target: Error) {
|
||||||
const captureStackTrace = Environment.getCaptureStackTrace();
|
const captureStackTrace = PlatformErrorPrototypeManipulation.getCaptureStackTrace();
|
||||||
if (!functionExists(captureStackTrace)) {
|
if (!isFunction(captureStackTrace)) {
|
||||||
// captureStackTrace is only available on V8, if it's not available
|
// captureStackTrace is only available on V8, if it's not available
|
||||||
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
// modern JS engines will usually generate a stack trace on error objects when they're thrown.
|
||||||
return;
|
return;
|
||||||
@@ -44,7 +51,7 @@ function ensureStackTrace(target: Error) {
|
|||||||
captureStackTrace(target, target.constructor);
|
captureStackTrace(target, target.constructor);
|
||||||
}
|
}
|
||||||
|
|
||||||
function functionExists(func: unknown): boolean {
|
// eslint-disable-next-line @typescript-eslint/ban-types
|
||||||
// Not doing truthy/falsy check i.e. if(func) as most values are truthy in JS for robustness
|
function isFunction(func: unknown): func is Function {
|
||||||
return typeof func === 'function';
|
return typeof func === 'function';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,9 +54,6 @@ export function assertInRange<T extends EnumType, TEnumValue extends EnumType>(
|
|||||||
value: TEnumValue,
|
value: TEnumValue,
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
) {
|
) {
|
||||||
if (value === undefined || value === null) {
|
|
||||||
throw new Error('absent enum value');
|
|
||||||
}
|
|
||||||
if (!(value in enumVariable)) {
|
if (!(value in enumVariable)) {
|
||||||
throw new RangeError(`enum value "${value}" is out of range`);
|
throw new RangeError(`enum value "${value}" is out of range`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,19 +9,16 @@ export abstract class ScriptingLanguageFactory<T> implements IScriptingLanguageF
|
|||||||
|
|
||||||
public create(language: ScriptingLanguage): T {
|
public create(language: ScriptingLanguage): T {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!this.getters.has(language)) {
|
const getter = this.getters.get(language);
|
||||||
|
if (!getter) {
|
||||||
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
throw new RangeError(`unknown language: "${ScriptingLanguage[language]}"`);
|
||||||
}
|
}
|
||||||
const getter = this.getters.get(language);
|
|
||||||
const instance = getter();
|
const instance = getter();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
protected registerGetter(language: ScriptingLanguage, getter: Getter<T>) {
|
||||||
assertInRange(language, ScriptingLanguage);
|
assertInRange(language, ScriptingLanguage);
|
||||||
if (!getter) {
|
|
||||||
throw new Error('missing getter');
|
|
||||||
}
|
|
||||||
if (this.getters.has(language)) {
|
if (this.getters.has(language)) {
|
||||||
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
throw new Error(`${ScriptingLanguage[language]} is already registered`);
|
||||||
}
|
}
|
||||||
|
|||||||
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
27
src/application/Common/Timing/BatchedDebounce.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
import { TimeoutType, Timer } from './Timer';
|
||||||
|
|
||||||
|
export function batchedDebounce<T>(
|
||||||
|
callback: (batches: readonly T[]) => void,
|
||||||
|
waitInMs: number,
|
||||||
|
timer: Timer = PlatformTimer,
|
||||||
|
): (arg: T) => void {
|
||||||
|
let lastTimeoutId: TimeoutType | undefined;
|
||||||
|
let batches: Array<T> = [];
|
||||||
|
|
||||||
|
return (arg: T) => {
|
||||||
|
batches.push(arg);
|
||||||
|
|
||||||
|
const later = () => {
|
||||||
|
callback(batches);
|
||||||
|
batches = [];
|
||||||
|
lastTimeoutId = undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (lastTimeoutId !== undefined) {
|
||||||
|
timer.clearTimeout(lastTimeoutId);
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTimeoutId = timer.setTimeout(later, waitInMs);
|
||||||
|
};
|
||||||
|
}
|
||||||
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
7
src/application/Common/Timing/PlatformTimer.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
import { Timer } from './Timer';
|
||||||
|
|
||||||
|
export const PlatformTimer: Timer = {
|
||||||
|
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
||||||
|
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
||||||
|
dateNow: () => Date.now(),
|
||||||
|
};
|
||||||
@@ -1,47 +1,29 @@
|
|||||||
|
import { Timer, TimeoutType } from './Timer';
|
||||||
|
import { PlatformTimer } from './PlatformTimer';
|
||||||
|
|
||||||
export type CallbackType = (..._: unknown[]) => void;
|
export type CallbackType = (..._: unknown[]) => void;
|
||||||
|
|
||||||
export function throttle(
|
export function throttle(
|
||||||
callback: CallbackType,
|
callback: CallbackType,
|
||||||
waitInMs: number,
|
waitInMs: number,
|
||||||
timer: ITimer = NodeTimer,
|
timer: Timer = PlatformTimer,
|
||||||
): CallbackType {
|
): CallbackType {
|
||||||
const throttler = new Throttler(timer, waitInMs, callback);
|
const throttler = new Throttler(timer, waitInMs, callback);
|
||||||
return (...args: unknown[]) => throttler.invoke(...args);
|
return (...args: unknown[]) => throttler.invoke(...args);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
class Throttler {
|
||||||
export type TimeoutType = ReturnType<typeof setTimeout>;
|
private queuedExecutionId: TimeoutType | undefined;
|
||||||
|
|
||||||
export interface ITimer {
|
|
||||||
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
|
||||||
clearTimeout: (timeoutId: TimeoutType) => void;
|
|
||||||
dateNow(): number;
|
|
||||||
}
|
|
||||||
|
|
||||||
const NodeTimer: ITimer = {
|
|
||||||
setTimeout: (callback, ms) => setTimeout(callback, ms),
|
|
||||||
clearTimeout: (timeoutId) => clearTimeout(timeoutId),
|
|
||||||
dateNow: () => Date.now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
interface IThrottler {
|
|
||||||
invoke: CallbackType;
|
|
||||||
}
|
|
||||||
|
|
||||||
class Throttler implements IThrottler {
|
|
||||||
private queuedExecutionId: TimeoutType;
|
|
||||||
|
|
||||||
private previouslyRun: number;
|
private previouslyRun: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly timer: ITimer,
|
private readonly timer: Timer,
|
||||||
private readonly waitInMs: number,
|
private readonly waitInMs: number,
|
||||||
private readonly callback: CallbackType,
|
private readonly callback: CallbackType,
|
||||||
) {
|
) {
|
||||||
if (!timer) { throw new Error('missing timer'); }
|
|
||||||
if (!waitInMs) { throw new Error('missing delay'); }
|
if (!waitInMs) { throw new Error('missing delay'); }
|
||||||
if (waitInMs < 0) { throw new Error('negative delay'); }
|
if (waitInMs < 0) { throw new Error('negative delay'); }
|
||||||
if (!callback) { throw new Error('missing callback'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public invoke(...args: unknown[]): void {
|
public invoke(...args: unknown[]): void {
|
||||||
8
src/application/Common/Timing/Timer.ts
Normal file
8
src/application/Common/Timing/Timer.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
// Allows aligning with both NodeJs (NodeJs.Timeout) and Window type (number)
|
||||||
|
export type TimeoutType = ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
|
export interface Timer {
|
||||||
|
setTimeout: (callback: () => void, ms: number) => TimeoutType;
|
||||||
|
clearTimeout: (timeoutId: TimeoutType) => void;
|
||||||
|
dateNow(): number;
|
||||||
|
}
|
||||||
@@ -26,7 +26,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
public readonly app: IApplication,
|
public readonly app: IApplication,
|
||||||
initialContext: OperatingSystem,
|
initialContext: OperatingSystem,
|
||||||
) {
|
) {
|
||||||
validateApp(app);
|
|
||||||
this.states = initializeStates(app);
|
this.states = initializeStates(app);
|
||||||
this.changeContext(initialContext);
|
this.changeContext(initialContext);
|
||||||
}
|
}
|
||||||
@@ -36,10 +35,8 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
if (this.currentOs === os) {
|
if (this.currentOs === os) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.collection = this.app.getCollection(os);
|
const collection = this.app.getCollection(os);
|
||||||
if (!this.collection) {
|
this.collection = collection;
|
||||||
throw new Error(`os "${OperatingSystem[os]}" is not defined in application`);
|
|
||||||
}
|
|
||||||
const event: IApplicationContextChangedEvent = {
|
const event: IApplicationContextChangedEvent = {
|
||||||
newState: this.states[os],
|
newState: this.states[os],
|
||||||
oldState: this.states[this.currentOs],
|
oldState: this.states[this.currentOs],
|
||||||
@@ -49,12 +46,6 @@ export class ApplicationContext implements IApplicationContext {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateApp(app: IApplication) {
|
|
||||||
if (!app) {
|
|
||||||
throw new Error('missing app');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeStates(app: IApplication): StateMachine {
|
function initializeStates(app: IApplication): StateMachine {
|
||||||
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
const machine = new Map<OperatingSystem, ICategoryCollectionState>();
|
||||||
for (const collection of app.collections) {
|
for (const collection of app.collections) {
|
||||||
|
|||||||
@@ -10,18 +10,23 @@ export async function buildContext(
|
|||||||
factory: IApplicationFactory = ApplicationFactory.Current,
|
factory: IApplicationFactory = ApplicationFactory.Current,
|
||||||
environment = RuntimeEnvironment.CurrentEnvironment,
|
environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
): Promise<IApplicationContext> {
|
): Promise<IApplicationContext> {
|
||||||
if (!factory) { throw new Error('missing factory'); }
|
|
||||||
if (!environment) { throw new Error('missing environment'); }
|
|
||||||
const app = await factory.getApp();
|
const app = await factory.getApp();
|
||||||
const os = getInitialOs(app, environment.os);
|
const os = getInitialOs(app, environment.os);
|
||||||
return new ApplicationContext(app, os);
|
return new ApplicationContext(app, os);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getInitialOs(app: IApplication, currentOs: OperatingSystem): OperatingSystem {
|
function getInitialOs(
|
||||||
|
app: IApplication,
|
||||||
|
currentOs: OperatingSystem | undefined,
|
||||||
|
): OperatingSystem {
|
||||||
const supportedOsList = app.getSupportedOsList();
|
const supportedOsList = app.getSupportedOsList();
|
||||||
if (supportedOsList.includes(currentOs)) {
|
if (currentOs !== undefined && supportedOsList.includes(currentOs)) {
|
||||||
return currentOs;
|
return currentOs;
|
||||||
}
|
}
|
||||||
|
return getMostSupportedOs(supportedOsList, app);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMostSupportedOs(supportedOsList: OperatingSystem[], app: IApplication) {
|
||||||
supportedOsList.sort((os1, os2) => {
|
supportedOsList.sort((os1, os2) => {
|
||||||
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
const getPriority = (os: OperatingSystem) => app.getCollection(os).totalScripts;
|
||||||
return getPriority(os2) - getPriority(os1);
|
return getPriority(os2) - getPriority(os1);
|
||||||
|
|||||||
@@ -4,23 +4,48 @@ import { UserFilter } from './Filter/UserFilter';
|
|||||||
import { IUserFilter } from './Filter/IUserFilter';
|
import { IUserFilter } from './Filter/IUserFilter';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
import { UserSelection } from './Selection/UserSelection';
|
import { UserSelection } from './Selection/UserSelection';
|
||||||
import { IUserSelection } from './Selection/IUserSelection';
|
|
||||||
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
import { ICategoryCollectionState } from './ICategoryCollectionState';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
import { UserSelectionFacade } from './Selection/UserSelectionFacade';
|
||||||
|
|
||||||
export class CategoryCollectionState implements ICategoryCollectionState {
|
export class CategoryCollectionState implements ICategoryCollectionState {
|
||||||
public readonly os: OperatingSystem;
|
public readonly os: OperatingSystem;
|
||||||
|
|
||||||
public readonly code: IApplicationCode;
|
public readonly code: IApplicationCode;
|
||||||
|
|
||||||
public readonly selection: IUserSelection;
|
public readonly selection: UserSelection;
|
||||||
|
|
||||||
public readonly filter: IUserFilter;
|
public readonly filter: IUserFilter;
|
||||||
|
|
||||||
public constructor(readonly collection: ICategoryCollection) {
|
public constructor(
|
||||||
this.selection = new UserSelection(collection, []);
|
public readonly collection: ICategoryCollection,
|
||||||
this.code = new ApplicationCode(this.selection, collection.scripting);
|
selectionFactory = DefaultSelectionFactory,
|
||||||
this.filter = new UserFilter(collection);
|
codeFactory = DefaultCodeFactory,
|
||||||
|
filterFactory = DefaultFilterFactory,
|
||||||
|
) {
|
||||||
|
this.selection = selectionFactory(collection, []);
|
||||||
|
this.code = codeFactory(this.selection.scripts, collection.scripting);
|
||||||
|
this.filter = filterFactory(collection);
|
||||||
this.os = collection.os;
|
this.os = collection.os;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type CodeFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ApplicationCode>
|
||||||
|
) => IApplicationCode;
|
||||||
|
|
||||||
|
const DefaultCodeFactory: CodeFactory = (...params) => new ApplicationCode(...params);
|
||||||
|
|
||||||
|
export type SelectionFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserSelectionFacade>
|
||||||
|
) => UserSelection;
|
||||||
|
|
||||||
|
const DefaultSelectionFactory: SelectionFactory = (
|
||||||
|
...params
|
||||||
|
) => new UserSelectionFacade(...params);
|
||||||
|
|
||||||
|
export type FilterFactory = (
|
||||||
|
...params: ConstructorParameters<typeof UserFilter>
|
||||||
|
) => IUserFilter;
|
||||||
|
|
||||||
|
const DefaultFilterFactory: FilterFactory = (...params) => new UserFilter(...params);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IReadOnlyUserSelection } from '@/application/Context/State/Selection/IUserSelection';
|
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
import { ReadonlyScriptSelection } from '@/application/Context/State/Selection/Script/ScriptSelection';
|
||||||
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
import { CodeChangedEvent } from './Event/CodeChangedEvent';
|
||||||
import { CodePosition } from './Position/CodePosition';
|
import { CodePosition } from './Position/CodePosition';
|
||||||
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
import { ICodeChangedEvent } from './Event/ICodeChangedEvent';
|
||||||
@@ -17,15 +17,12 @@ export class ApplicationCode implements IApplicationCode {
|
|||||||
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
private scriptPositions = new Map<SelectedScript, CodePosition>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
userSelection: IReadOnlyUserSelection,
|
selection: ReadonlyScriptSelection,
|
||||||
private readonly scriptingDefinition: IScriptingDefinition,
|
private readonly scriptingDefinition: IScriptingDefinition,
|
||||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
private readonly generator: IUserScriptGenerator = new UserScriptGenerator(),
|
||||||
) {
|
) {
|
||||||
if (!userSelection) { throw new Error('missing userSelection'); }
|
this.setCode(selection.selectedScripts);
|
||||||
if (!scriptingDefinition) { throw new Error('missing scriptingDefinition'); }
|
selection.changed.on((scripts) => {
|
||||||
if (!generator) { throw new Error('missing generator'); }
|
|
||||||
this.setCode(userSelection.selectedScripts);
|
|
||||||
userSelection.changed.on((scripts) => {
|
|
||||||
this.setCode(scripts);
|
this.setCode(scripts);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { SelectedScript } from '../../Selection/SelectedScript';
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
import { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
@@ -36,7 +36,18 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: IScript): ICodePosition {
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
return this.scripts.get(script);
|
return this.getPositionById(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
|
const position = [...this.scripts.entries()]
|
||||||
|
.filter(([s]) => s.id === scriptId)
|
||||||
|
.map(([, pos]) => pos)
|
||||||
|
.at(0);
|
||||||
|
if (!position) {
|
||||||
|
throw new Error('Unknown script: Position could not be found for the script');
|
||||||
|
}
|
||||||
|
return position;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,9 +3,9 @@ import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePo
|
|||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
addedScripts: ReadonlyArray<IScript>;
|
readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
removedScripts: ReadonlyArray<IScript>;
|
readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
changedScripts: ReadonlyArray<IScript>;
|
readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: IScript): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,7 +16,9 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = code.match(/[^\r\n]+/g);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
this.lines.push(...lines);
|
if (lines) {
|
||||||
|
this.lines.push(...lines);
|
||||||
|
}
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
|
|
||||||
export interface IUserScript {
|
export interface IUserScript {
|
||||||
code: string;
|
readonly code: string;
|
||||||
scriptPositions: Map<SelectedScript, ICodePosition>;
|
readonly scriptPositions: Map<SelectedScript, ICodePosition>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
|
|
||||||
export interface IUserScriptGenerator {
|
export interface IUserScriptGenerator {
|
||||||
buildCode(
|
buildCode(
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
scriptingDefinition: IScriptingDefinition,
|
||||||
|
): IUserScript;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { SelectedScript } from '@/application/Context/State/Selection/SelectedScript';
|
|
||||||
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { CodePosition } from '../Position/CodePosition';
|
import { CodePosition } from '../Position/CodePosition';
|
||||||
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
import { IUserScriptGenerator } from './IUserScriptGenerator';
|
||||||
import { IUserScript } from './IUserScript';
|
import { IUserScript } from './IUserScript';
|
||||||
@@ -17,8 +17,6 @@ export class UserScriptGenerator implements IUserScriptGenerator {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
scriptingDefinition: IScriptingDefinition,
|
scriptingDefinition: IScriptingDefinition,
|
||||||
): IUserScript {
|
): IUserScript {
|
||||||
if (!selectedScripts) { throw new Error('missing scripts'); }
|
|
||||||
if (!scriptingDefinition) { throw new Error('missing definition'); }
|
|
||||||
if (!selectedScripts.length) {
|
if (!selectedScripts.length) {
|
||||||
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
return { code: '', scriptPositions: new Map<SelectedScript, ICodePosition>() };
|
||||||
}
|
}
|
||||||
@@ -68,8 +66,19 @@ function appendSelection(
|
|||||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
function appendCode(selection: SelectedScript, builder: ICodeBuilder): ICodeBuilder {
|
||||||
const { script } = selection;
|
const { script } = selection;
|
||||||
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
const name = selection.revert ? `${script.name} (revert)` : script.name;
|
||||||
const scriptCode = selection.revert ? script.code.revert : script.code.execute;
|
const scriptCode = getSelectedCode(selection);
|
||||||
return builder
|
return builder
|
||||||
.appendLine()
|
.appendLine()
|
||||||
.appendFunction(name, scriptCode);
|
.appendFunction(name, scriptCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSelectedCode(selection: SelectedScript): string {
|
||||||
|
const { code } = selection.script;
|
||||||
|
if (!selection.revert) {
|
||||||
|
return code.execute;
|
||||||
|
}
|
||||||
|
if (!code.revert) {
|
||||||
|
throw new Error('Reverted script lacks revert code.');
|
||||||
|
}
|
||||||
|
return code.revert;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,37 +1,37 @@
|
|||||||
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult';
|
||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
import { IFilterChangeDetails, IFilterChangeDetailsVisitor } from './IFilterChangeDetails';
|
import {
|
||||||
|
IFilterChangeDetails, IFilterChangeDetailsVisitor,
|
||||||
|
ApplyFilterAction, ClearFilterAction,
|
||||||
|
} from './IFilterChangeDetails';
|
||||||
|
|
||||||
export class FilterChange implements IFilterChangeDetails {
|
export class FilterChange implements IFilterChangeDetails {
|
||||||
public static forApply(filter: IFilterResult) {
|
public static forApply(
|
||||||
if (!filter) {
|
filter: IFilterResult,
|
||||||
throw new Error('missing filter');
|
): IFilterChangeDetails {
|
||||||
}
|
return new FilterChange({ type: FilterActionType.Apply, filter });
|
||||||
return new FilterChange(FilterActionType.Apply, filter);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static forClear() {
|
public static forClear(): IFilterChangeDetails {
|
||||||
return new FilterChange(FilterActionType.Clear);
|
return new FilterChange({ type: FilterActionType.Clear });
|
||||||
}
|
}
|
||||||
|
|
||||||
private constructor(
|
private constructor(public readonly action: ApplyFilterAction | ClearFilterAction) { }
|
||||||
public readonly actionType: FilterActionType,
|
|
||||||
public readonly filter?: IFilterResult,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
public visit(visitor: IFilterChangeDetailsVisitor): void {
|
||||||
if (!visitor) {
|
switch (this.action.type) {
|
||||||
throw new Error('missing visitor');
|
|
||||||
}
|
|
||||||
switch (this.actionType) {
|
|
||||||
case FilterActionType.Apply:
|
case FilterActionType.Apply:
|
||||||
visitor.onApply(this.filter);
|
if (visitor.onApply) {
|
||||||
|
visitor.onApply(this.action.filter);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case FilterActionType.Clear:
|
case FilterActionType.Clear:
|
||||||
visitor.onClear();
|
if (visitor.onClear) {
|
||||||
|
visitor.onClear();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new Error(`Unknown action type: ${this.actionType}`);
|
throw new Error(`Unknown action: ${this.action}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,22 @@ import { IFilterResult } from '@/application/Context/State/Filter/IFilterResult'
|
|||||||
import { FilterActionType } from './FilterActionType';
|
import { FilterActionType } from './FilterActionType';
|
||||||
|
|
||||||
export interface IFilterChangeDetails {
|
export interface IFilterChangeDetails {
|
||||||
readonly actionType: FilterActionType;
|
readonly action: FilterAction;
|
||||||
readonly filter?: IFilterResult;
|
|
||||||
|
|
||||||
visit(visitor: IFilterChangeDetailsVisitor): void;
|
visit(visitor: IFilterChangeDetailsVisitor): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFilterChangeDetailsVisitor {
|
export interface IFilterChangeDetailsVisitor {
|
||||||
onClear(): void;
|
readonly onClear?: () => void;
|
||||||
onApply(filter: IFilterResult): void;
|
readonly onApply?: (filter: IFilterResult) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type ApplyFilterAction = {
|
||||||
|
readonly type: FilterActionType.Apply,
|
||||||
|
readonly filter: IFilterResult;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ClearFilterAction = {
|
||||||
|
readonly type: FilterActionType.Clear,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterAction = ApplyFilterAction | ClearFilterAction;
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ export class FilterResult implements IFilterResult {
|
|||||||
public readonly query: string,
|
public readonly query: string,
|
||||||
) {
|
) {
|
||||||
if (!query) { throw new Error('Query is empty or undefined'); }
|
if (!query) { throw new Error('Query is empty or undefined'); }
|
||||||
if (!scriptMatches) { throw new Error('Script matches is undefined'); }
|
|
||||||
if (!categoryMatches) { throw new Error('Category matches is undefined'); }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public hasAnyMatches(): boolean {
|
public hasAnyMatches(): boolean {
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
import { IReadOnlyUserFilter, IUserFilter } from './Filter/IUserFilter';
|
||||||
import { IReadOnlyUserSelection, IUserSelection } from './Selection/IUserSelection';
|
import { ReadonlyUserSelection, UserSelection } from './Selection/UserSelection';
|
||||||
import { IApplicationCode } from './Code/IApplicationCode';
|
import { IApplicationCode } from './Code/IApplicationCode';
|
||||||
|
|
||||||
export interface IReadOnlyCategoryCollectionState {
|
export interface IReadOnlyCategoryCollectionState {
|
||||||
readonly code: IApplicationCode;
|
readonly code: IApplicationCode;
|
||||||
readonly os: OperatingSystem;
|
readonly os: OperatingSystem;
|
||||||
readonly filter: IReadOnlyUserFilter;
|
readonly filter: IReadOnlyUserFilter;
|
||||||
readonly selection: IReadOnlyUserSelection;
|
readonly selection: ReadonlyUserSelection;
|
||||||
readonly collection: ICategoryCollection;
|
readonly collection: ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
export interface ICategoryCollectionState extends IReadOnlyCategoryCollectionState {
|
||||||
readonly filter: IUserFilter;
|
readonly filter: IUserFilter;
|
||||||
readonly selection: IUserSelection;
|
readonly selection: UserSelection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyCategorySelection {
|
||||||
|
areAllScriptsSelected(category: ICategory): boolean;
|
||||||
|
isAnyScriptSelected(category: ICategory): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||||
|
processChanges(action: CategorySelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
type CategorySelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface CategorySelectionChange {
|
||||||
|
readonly categoryId: number;
|
||||||
|
readonly newStatus: CategorySelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CategorySelectionChangeCommand {
|
||||||
|
readonly changes: readonly CategorySelectionChange[];
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import { ICategory } from '@/domain/ICategory';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
|
import { ScriptSelectionChange } from '../Script/ScriptSelectionChange';
|
||||||
|
import { CategorySelection } from './CategorySelection';
|
||||||
|
import { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
|
export class ScriptToCategorySelectionMapper implements CategorySelection {
|
||||||
|
constructor(
|
||||||
|
private readonly scriptSelection: ScriptSelection,
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public areAllScriptsSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
if (selectedScripts.length < scripts.length) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return scripts.every(
|
||||||
|
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isAnyScriptSelected(category: ICategory): boolean {
|
||||||
|
const { selectedScripts } = this.scriptSelection;
|
||||||
|
if (selectedScripts.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return selectedScripts.some((s) => category.includes(s.script));
|
||||||
|
}
|
||||||
|
|
||||||
|
public processChanges(action: CategorySelectionChangeCommand): void {
|
||||||
|
const scriptChanges = action.changes.reduce((changes, change) => {
|
||||||
|
changes.push(...this.collectScriptChanges(change));
|
||||||
|
return changes;
|
||||||
|
}, new Array<ScriptSelectionChange>());
|
||||||
|
this.scriptSelection.processChanges({
|
||||||
|
changes: scriptChanges,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private collectScriptChanges(change: CategorySelectionChange): ScriptSelectionChange[] {
|
||||||
|
const category = this.collection.getCategory(change.categoryId);
|
||||||
|
const scripts = category.getAllScriptsRecursively();
|
||||||
|
const scriptsChangesInCategory = scripts
|
||||||
|
.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
...change.newStatus,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
return scriptsChangesInCategory;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
import { IScript } from '@/domain/IScript';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export interface IReadOnlyUserSelection {
|
|
||||||
readonly changed: IEventSource<ReadonlyArray<SelectedScript>>;
|
|
||||||
readonly selectedScripts: ReadonlyArray<SelectedScript>;
|
|
||||||
isSelected(scriptId: string): boolean;
|
|
||||||
areAllSelected(category: ICategory): boolean;
|
|
||||||
isAnySelected(category: ICategory): boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IUserSelection extends IReadOnlyUserSelection {
|
|
||||||
removeAllInCategory(categoryId: number): void;
|
|
||||||
addOrUpdateAllInCategory(categoryId: number, revert: boolean): void;
|
|
||||||
addSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
addOrUpdateSelectedScript(scriptId: string, revert: boolean): void;
|
|
||||||
removeSelectedScript(scriptId: string): void;
|
|
||||||
selectOnly(scripts: ReadonlyArray<IScript>): void;
|
|
||||||
selectAll(): void;
|
|
||||||
deselectAll(): void;
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,171 @@
|
|||||||
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
|
import { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
|
import { ScriptSelection } from './ScriptSelection';
|
||||||
|
import { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
|
|
||||||
|
const DEBOUNCE_DELAY_IN_MS = 100;
|
||||||
|
|
||||||
|
export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeCommand>;
|
||||||
|
|
||||||
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
|
private readonly scripts: Repository<string, SelectedScript>;
|
||||||
|
|
||||||
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private readonly collection: ICategoryCollection,
|
||||||
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
|
) {
|
||||||
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
|
for (const script of selectedScripts) {
|
||||||
|
this.scripts.addItem(script);
|
||||||
|
}
|
||||||
|
this.processChanges = debounce(
|
||||||
|
(batchedRequests: readonly ScriptSelectionChangeCommand[]) => {
|
||||||
|
const consolidatedChanges = batchedRequests.flatMap((request) => request.changes);
|
||||||
|
this.processScriptChanges(consolidatedChanges);
|
||||||
|
},
|
||||||
|
DEBOUNCE_DELAY_IN_MS,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public isSelected(scriptId: string): boolean {
|
||||||
|
return this.scripts.exists(scriptId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
|
return this.scripts.getItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectAll(): void {
|
||||||
|
const scriptsToSelect = this.collection
|
||||||
|
.getAllScripts()
|
||||||
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
|
if (scriptsToSelect.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: scriptsToSelect.map((script): ScriptSelectionChange => ({
|
||||||
|
scriptId: script.id,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public deselectAll(): void {
|
||||||
|
if (this.scripts.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
||||||
|
this.processChanges({
|
||||||
|
changes: selectedScriptIds.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
|
if (scripts.length === 0) {
|
||||||
|
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||||
|
}
|
||||||
|
this.processChanges({
|
||||||
|
changes: [
|
||||||
|
...getScriptIdsToBeDeselected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
...getScriptIdsToBeSelected(this.scripts, scripts)
|
||||||
|
.map((scriptId): ScriptSelectionChange => ({
|
||||||
|
scriptId,
|
||||||
|
newStatus: {
|
||||||
|
isSelected: true,
|
||||||
|
isReverted: false,
|
||||||
|
},
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private processScriptChanges(changes: readonly ScriptSelectionChange[]): void {
|
||||||
|
let totalChanged = 0;
|
||||||
|
for (const change of changes) {
|
||||||
|
totalChanged += this.applyChange(change);
|
||||||
|
}
|
||||||
|
if (totalChanged > 0) {
|
||||||
|
this.changed.notify(this.scripts.getItems());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
|
const script = this.collection.getScript(change.scriptId);
|
||||||
|
if (change.newStatus.isSelected) {
|
||||||
|
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||||
|
}
|
||||||
|
return this.removeScript(script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
|
const script = this.collection.getScript(scriptId);
|
||||||
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
|
this.scripts.addItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
const existingSelectedScript = this.scripts.getById(selectedScript.id);
|
||||||
|
if (equals(selectedScript, existingSelectedScript)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.addOrUpdateItem(selectedScript);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeScript(scriptId: string): number {
|
||||||
|
if (!this.scripts.exists(scriptId)) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
this.scripts.removeItem(scriptId);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeSelected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return desiredScripts
|
||||||
|
.filter((script) => !existingItems.exists(script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getScriptIdsToBeDeselected(
|
||||||
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
|
desiredScripts: readonly IScript[],
|
||||||
|
): string[] {
|
||||||
|
return existingItems
|
||||||
|
.getItems()
|
||||||
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||||
|
.map((script) => script.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
|
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
import { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
|
export interface ReadonlyScriptSelection {
|
||||||
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
|
isSelected(scriptId: string): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
|
selectOnly(scripts: readonly IScript[]): void;
|
||||||
|
selectAll(): void;
|
||||||
|
deselectAll(): void;
|
||||||
|
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
export type ScriptSelectionStatus = {
|
||||||
|
readonly isSelected: true;
|
||||||
|
readonly isReverted: boolean;
|
||||||
|
} | {
|
||||||
|
readonly isSelected: false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ScriptSelectionChange {
|
||||||
|
readonly scriptId: string;
|
||||||
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ScriptSelectionChangeCommand {
|
||||||
|
readonly changes: ReadonlyArray<ScriptSelectionChange>;
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
|
import { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
|
type ScriptId = IScript['id'];
|
||||||
|
|
||||||
|
export interface SelectedScript extends IEntity<ScriptId> {
|
||||||
|
readonly script: IScript;
|
||||||
|
readonly revert: boolean;
|
||||||
|
}
|
||||||
@@ -1,14 +1,17 @@
|
|||||||
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { IScript } from '@/domain/IScript';
|
||||||
|
import { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class SelectedScript extends BaseEntity<string> {
|
type SelectedScriptId = SelectedScript['id'];
|
||||||
|
|
||||||
|
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: IScript,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
super(script.id);
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error('cannot revert an irreversible script');
|
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,167 +1,12 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { CategorySelection, ReadonlyCategorySelection } from './Category/CategorySelection';
|
||||||
import { IScript } from '@/domain/IScript';
|
import { ReadonlyScriptSelection, ScriptSelection } from './Script/ScriptSelection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
|
||||||
import { IRepository } from '@/infrastructure/Repository/IRepository';
|
|
||||||
import { ICategory } from '@/domain/ICategory';
|
|
||||||
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
|
||||||
import { IUserSelection } from './IUserSelection';
|
|
||||||
import { SelectedScript } from './SelectedScript';
|
|
||||||
|
|
||||||
export class UserSelection implements IUserSelection {
|
export interface ReadonlyUserSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
readonly categories: ReadonlyCategorySelection;
|
||||||
|
readonly scripts: ReadonlyScriptSelection;
|
||||||
private readonly scripts: IRepository<string, SelectedScript>;
|
}
|
||||||
|
|
||||||
constructor(
|
export interface UserSelection extends ReadonlyUserSelection {
|
||||||
private readonly collection: ICategoryCollection,
|
readonly categories: CategorySelection;
|
||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
readonly scripts: ScriptSelection;
|
||||||
) {
|
|
||||||
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
|
||||||
for (const script of selectedScripts) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public areAllSelected(category: ICategory): boolean {
|
|
||||||
if (this.selectedScripts.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const scripts = category.getAllScriptsRecursively();
|
|
||||||
if (this.selectedScripts.length < scripts.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return scripts.every(
|
|
||||||
(script) => this.selectedScripts.some((selected) => selected.id === script.id),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public isAnySelected(category: ICategory): boolean {
|
|
||||||
if (this.selectedScripts.length === 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return this.selectedScripts.some((s) => category.includes(s.script));
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeAllInCategory(categoryId: number): void {
|
|
||||||
const category = this.collection.findCategory(categoryId);
|
|
||||||
const scriptsToRemove = category.getAllScriptsRecursively()
|
|
||||||
.filter((script) => this.scripts.exists(script.id));
|
|
||||||
if (!scriptsToRemove.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToRemove) {
|
|
||||||
this.scripts.removeItem(script.id);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateAllInCategory(categoryId: number, revert = false): void {
|
|
||||||
const scriptsToAddOrUpdate = this.collection
|
|
||||||
.findCategory(categoryId)
|
|
||||||
.getAllScriptsRecursively()
|
|
||||||
.filter(
|
|
||||||
(script) => !this.scripts.exists(script.id)
|
|
||||||
|| this.scripts.getById(script.id).revert !== revert,
|
|
||||||
)
|
|
||||||
.map((script) => new SelectedScript(script, revert));
|
|
||||||
if (!scriptsToAddOrUpdate.length) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToAddOrUpdate) {
|
|
||||||
this.scripts.addOrUpdateItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
if (!script) {
|
|
||||||
throw new Error(`Cannot add (id: ${scriptId}) as it is unknown`);
|
|
||||||
}
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public addOrUpdateSelectedScript(scriptId: string, revert: boolean): void {
|
|
||||||
const script = this.collection.findScript(scriptId);
|
|
||||||
const selectedScript = new SelectedScript(script, revert);
|
|
||||||
this.scripts.addOrUpdateItem(selectedScript);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public removeSelectedScript(scriptId: string): void {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public isSelected(scriptId: string): boolean {
|
|
||||||
return this.scripts.exists(scriptId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Get users scripts based on his/her selections */
|
|
||||||
public get selectedScripts(): ReadonlyArray<SelectedScript> {
|
|
||||||
return this.scripts.getItems();
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectAll(): void {
|
|
||||||
const scriptsToSelect = this.collection
|
|
||||||
.getAllScripts()
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
if (scriptsToSelect.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
for (const script of scriptsToSelect) {
|
|
||||||
this.scripts.addItem(script);
|
|
||||||
}
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
|
|
||||||
public deselectAll(): void {
|
|
||||||
if (this.scripts.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const selectedScriptIds = this.scripts.getItems().map((script) => script.id);
|
|
||||||
for (const scriptId of selectedScriptIds) {
|
|
||||||
this.scripts.removeItem(scriptId);
|
|
||||||
}
|
|
||||||
this.changed.notify([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
public selectOnly(scripts: readonly IScript[]): void {
|
|
||||||
if (!scripts || scripts.length === 0) {
|
|
||||||
throw new Error('Scripts are empty. Use deselectAll() if you want to deselect everything');
|
|
||||||
}
|
|
||||||
let totalChanged = 0;
|
|
||||||
totalChanged += this.unselectMissingWithoutNotifying(scripts);
|
|
||||||
totalChanged += this.selectNewWithoutNotifying(scripts);
|
|
||||||
if (totalChanged > 0) {
|
|
||||||
this.changed.notify(this.scripts.getItems());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private unselectMissingWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
if (this.scripts.length === 0 || scripts.length === 0) {
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
const existingItems = this.scripts.getItems();
|
|
||||||
const missingIds = existingItems
|
|
||||||
.filter((existing) => !scripts.some((script) => existing.id === script.id))
|
|
||||||
.map((script) => script.id);
|
|
||||||
for (const id of missingIds) {
|
|
||||||
this.scripts.removeItem(id);
|
|
||||||
}
|
|
||||||
return missingIds.length;
|
|
||||||
}
|
|
||||||
|
|
||||||
private selectNewWithoutNotifying(scripts: readonly IScript[]): number {
|
|
||||||
const unselectedScripts = scripts
|
|
||||||
.filter((script) => !this.scripts.exists(script.id))
|
|
||||||
.map((script) => new SelectedScript(script, false));
|
|
||||||
for (const newScript of unselectedScripts) {
|
|
||||||
this.scripts.addItem(newScript);
|
|
||||||
}
|
|
||||||
return unselectedScripts.length;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
|
import { CategorySelection } from './Category/CategorySelection';
|
||||||
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
|
import { ScriptSelection } from './Script/ScriptSelection';
|
||||||
|
import { UserSelection } from './UserSelection';
|
||||||
|
import { SelectedScript } from './Script/SelectedScript';
|
||||||
|
|
||||||
|
export class UserSelectionFacade implements UserSelection {
|
||||||
|
public readonly categories: CategorySelection;
|
||||||
|
|
||||||
|
public readonly scripts: ScriptSelection;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
collection: ICategoryCollection,
|
||||||
|
selectedScripts: readonly SelectedScript[],
|
||||||
|
scriptsFactory = DefaultScriptsFactory,
|
||||||
|
categoriesFactory = DefaultCategoriesFactory,
|
||||||
|
) {
|
||||||
|
this.scripts = scriptsFactory(collection, selectedScripts);
|
||||||
|
this.categories = categoriesFactory(this.scripts, collection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ScriptsFactory = (
|
||||||
|
...params: ConstructorParameters<typeof DebouncedScriptSelection>
|
||||||
|
) => ScriptSelection;
|
||||||
|
|
||||||
|
const DefaultScriptsFactory: ScriptsFactory = (
|
||||||
|
...params
|
||||||
|
) => new DebouncedScriptSelection(...params);
|
||||||
|
|
||||||
|
export type CategoriesFactory = (
|
||||||
|
...params: ConstructorParameters<typeof ScriptToCategorySelectionMapper>
|
||||||
|
) => CategorySelection;
|
||||||
|
|
||||||
|
const DefaultCategoriesFactory: CategoriesFactory = (
|
||||||
|
...params
|
||||||
|
) => new ScriptToCategorySelectionMapper(...params);
|
||||||
@@ -32,10 +32,7 @@ const PreParsedCollections: readonly CollectionData [] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(collections: readonly CollectionData[]) {
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
if (!collections?.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.some((collection) => !collection)) {
|
|
||||||
throw new Error('missing collection provided');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,10 +28,7 @@ export function parseCategoryCollection(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validate(content: CollectionData): void {
|
function validate(content: CollectionData): void {
|
||||||
if (!content) {
|
if (!content.actions.length) {
|
||||||
throw new Error('missing content');
|
|
||||||
}
|
|
||||||
if (!content.actions || content.actions.length <= 0) {
|
|
||||||
throw new Error('content does not define any action');
|
throw new Error('content does not define any action');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type {
|
import type {
|
||||||
CategoryData, ScriptData, CategoryOrScriptData, InstructionHolder,
|
CategoryData, ScriptData, CategoryOrScriptData,
|
||||||
} from '@/application/collections/';
|
} from '@/application/collections/';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
import { Category } from '@/domain/Category';
|
import { Category } from '@/domain/Category';
|
||||||
@@ -16,7 +16,6 @@ export function parseCategory(
|
|||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
factory: CategoryFactoryType = CategoryFactory,
|
factory: CategoryFactoryType = CategoryFactory,
|
||||||
): Category {
|
): Category {
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
return parseCategoryRecursively({
|
return parseCategoryRecursively({
|
||||||
categoryData: category,
|
categoryData: category,
|
||||||
context,
|
context,
|
||||||
@@ -30,8 +29,8 @@ interface ICategoryParseContext {
|
|||||||
readonly factory: CategoryFactoryType,
|
readonly factory: CategoryFactoryType,
|
||||||
readonly parentCategory?: CategoryData,
|
readonly parentCategory?: CategoryData,
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
function parseCategoryRecursively(context: ICategoryParseContext): Category | never {
|
||||||
ensureValidCategory(context.categoryData, context.parentCategory);
|
ensureValidCategory(context.categoryData, context.parentCategory);
|
||||||
const children: ICategoryChildren = {
|
const children: ICategoryChildren = {
|
||||||
subCategories: new Array<Category>(),
|
subCategories: new Array<Category>(),
|
||||||
@@ -55,7 +54,7 @@ function parseCategoryRecursively(context: ICategoryParseContext): Category {
|
|||||||
/* scripts: */ children.subScripts,
|
/* scripts: */ children.subScripts,
|
||||||
);
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
new NodeValidator({
|
return new NodeValidator({
|
||||||
type: NodeType.Category,
|
type: NodeType.Category,
|
||||||
selfNode: context.categoryData,
|
selfNode: context.categoryData,
|
||||||
parentNode: context.parentCategory,
|
parentNode: context.parentCategory,
|
||||||
@@ -72,7 +71,7 @@ function ensureValidCategory(category: CategoryData, parentCategory?: CategoryDa
|
|||||||
.assertDefined(category)
|
.assertDefined(category)
|
||||||
.assertValidName(category.category)
|
.assertValidName(category.category)
|
||||||
.assert(
|
.assert(
|
||||||
() => category.children && category.children.length > 0,
|
() => category.children.length > 0,
|
||||||
`"${category.category}" has no children.`,
|
`"${category.category}" has no children.`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -94,14 +93,14 @@ function parseNode(context: INodeParseContext) {
|
|||||||
validator.assertDefined(context.nodeData);
|
validator.assertDefined(context.nodeData);
|
||||||
if (isCategory(context.nodeData)) {
|
if (isCategory(context.nodeData)) {
|
||||||
const subCategory = parseCategoryRecursively({
|
const subCategory = parseCategoryRecursively({
|
||||||
categoryData: context.nodeData as CategoryData,
|
categoryData: context.nodeData,
|
||||||
context: context.context,
|
context: context.context,
|
||||||
factory: context.factory,
|
factory: context.factory,
|
||||||
parentCategory: context.parent,
|
parentCategory: context.parent,
|
||||||
});
|
});
|
||||||
context.children.subCategories.push(subCategory);
|
context.children.subCategories.push(subCategory);
|
||||||
} else if (isScript(context.nodeData)) {
|
} else if (isScript(context.nodeData)) {
|
||||||
const script = parseScript(context.nodeData as ScriptData, context.context);
|
const script = parseScript(context.nodeData, context.context);
|
||||||
context.children.subScripts.push(script);
|
context.children.subScripts.push(script);
|
||||||
} else {
|
} else {
|
||||||
validator.throw('Node is neither a category or a script.');
|
validator.throw('Node is neither a category or a script.');
|
||||||
@@ -109,19 +108,18 @@ function parseNode(context: INodeParseContext) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
const holder = (data as InstructionHolder);
|
return hasCode(data) || hasCall(data);
|
||||||
return hasCode(holder) || hasCall(holder);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
return hasProperty(data, 'category');
|
return hasProperty(data, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: InstructionHolder): boolean {
|
function hasCode(data: unknown): boolean {
|
||||||
return hasProperty(data, 'code');
|
return hasProperty(data, 'code');
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: InstructionHolder) {
|
function hasCall(data: unknown) {
|
||||||
return hasProperty(data, 'call');
|
return hasProperty(data, 'call');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
import type { DocumentableData, DocumentationData } from '@/application/collections/';
|
||||||
|
|
||||||
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
export function parseDocs(documentable: DocumentableData): readonly string[] {
|
||||||
if (!documentable) {
|
|
||||||
throw new Error('missing documentable');
|
|
||||||
}
|
|
||||||
const { docs } = documentable;
|
const { docs } = documentable;
|
||||||
if (!docs) {
|
if (!docs) {
|
||||||
return [];
|
return [];
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ export class NodeValidator {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public throw(errorMessage: string) {
|
public throw(errorMessage: string): never {
|
||||||
throw new NodeDataError(errorMessage, this.context);
|
throw new NodeDataError(errorMessage, this.context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,8 +17,7 @@ export class CategoryCollectionParseContext implements ICategoryCollectionParseC
|
|||||||
scripting: IScriptingDefinition,
|
scripting: IScriptingDefinition,
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
) {
|
) {
|
||||||
if (!scripting) { throw new Error('missing scripting'); }
|
|
||||||
this.syntax = syntaxFactory.create(scripting.language);
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
this.compiler = new ScriptCompiler(functionsData, this.syntax);
|
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,19 +15,10 @@ export class Expression implements IExpression {
|
|||||||
public readonly evaluator: ExpressionEvaluator,
|
public readonly evaluator: ExpressionEvaluator,
|
||||||
parameters?: IReadOnlyFunctionParameterCollection,
|
parameters?: IReadOnlyFunctionParameterCollection,
|
||||||
) {
|
) {
|
||||||
if (!position) {
|
|
||||||
throw new Error('missing position');
|
|
||||||
}
|
|
||||||
if (!evaluator) {
|
|
||||||
throw new Error('missing evaluator');
|
|
||||||
}
|
|
||||||
this.parameters = parameters ?? new FunctionParameterCollection();
|
this.parameters = parameters ?? new FunctionParameterCollection();
|
||||||
}
|
}
|
||||||
|
|
||||||
public evaluate(context: IExpressionEvaluationContext): string {
|
public evaluate(context: IExpressionEvaluationContext): string {
|
||||||
if (!context) {
|
|
||||||
throw new Error('missing context');
|
|
||||||
}
|
|
||||||
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
validateThatAllRequiredParametersAreSatisfied(this.parameters, context.args);
|
||||||
const args = filterUnusedArguments(this.parameters, context.args);
|
const args = filterUnusedArguments(this.parameters, context.args);
|
||||||
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
const filteredContext = new ExpressionEvaluationContext(args, context.pipelineCompiler);
|
||||||
|
|||||||
@@ -12,8 +12,5 @@ export class ExpressionEvaluationContext implements IExpressionEvaluationContext
|
|||||||
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
public readonly args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
public readonly pipelineCompiler: IPipelineCompiler = new PipelineCompiler(),
|
||||||
) {
|
) {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
|
|
||||||
|
export function createPositionFromRegexFullMatch(
|
||||||
|
match: RegExpMatchArray,
|
||||||
|
): ExpressionPosition {
|
||||||
|
const startPos = match.index;
|
||||||
|
if (startPos === undefined) {
|
||||||
|
throw new Error(`Regex match did not yield any results: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const fullMatch = match[0];
|
||||||
|
if (!fullMatch.length) {
|
||||||
|
throw new Error(`Regex match is empty: ${JSON.stringify(match)}`);
|
||||||
|
}
|
||||||
|
const endPos = startPos + fullMatch.length;
|
||||||
|
return new ExpressionPosition(startPos, endPos);
|
||||||
|
}
|
||||||
@@ -11,14 +11,11 @@ export class ExpressionsCompiler implements IExpressionsCompiler {
|
|||||||
) { }
|
) { }
|
||||||
|
|
||||||
public compileExpressions(
|
public compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection,
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): string {
|
): string {
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args, send empty collection instead.');
|
|
||||||
}
|
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return code;
|
return '';
|
||||||
}
|
}
|
||||||
const context = new ExpressionEvaluationContext(args);
|
const context = new ExpressionEvaluationContext(args);
|
||||||
const compiledCode = compileRecursively(code, context, this.extractor);
|
const compiledCode = compileRecursively(code, context, this.extractor);
|
||||||
@@ -145,7 +142,7 @@ function ensureParamsUsedInCodeHasArgsProvided(
|
|||||||
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
providedArgs: IReadOnlyFunctionCallArgumentCollection,
|
||||||
): void {
|
): void {
|
||||||
const usedParameterNames = extractRequiredParameterNames(expressions);
|
const usedParameterNames = extractRequiredParameterNames(expressions);
|
||||||
if (!usedParameterNames?.length) {
|
if (!usedParameterNames.length) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const notProvidedParameters = usedParameterNames
|
const notProvidedParameters = usedParameterNames
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argume
|
|||||||
|
|
||||||
export interface IExpressionsCompiler {
|
export interface IExpressionsCompiler {
|
||||||
compileExpressions(
|
compileExpressions(
|
||||||
code: string | undefined,
|
code: string,
|
||||||
args: IReadOnlyFunctionCallArgumentCollection): string;
|
args: IReadOnlyFunctionCallArgumentCollection,
|
||||||
|
): string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,9 @@ const Parsers = [
|
|||||||
|
|
||||||
export class CompositeExpressionParser implements IExpressionParser {
|
export class CompositeExpressionParser implements IExpressionParser {
|
||||||
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
public constructor(private readonly leafs: readonly IExpressionParser[] = Parsers) {
|
||||||
if (!leafs) {
|
if (!leafs.length) {
|
||||||
throw new Error('missing leafs');
|
throw new Error('missing leafs');
|
||||||
}
|
}
|
||||||
if (leafs.some((leaf) => !leaf)) {
|
|
||||||
throw new Error('missing leaf');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { IExpressionParser } from '../IExpressionParser';
|
import { IExpressionParser } from '../IExpressionParser';
|
||||||
import { ExpressionPosition } from '../../Expression/ExpressionPosition';
|
|
||||||
import { IExpression } from '../../Expression/IExpression';
|
import { IExpression } from '../../Expression/IExpression';
|
||||||
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
import { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '../../../Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export abstract class RegexParser implements IExpressionParser {
|
export abstract class RegexParser implements IExpressionParser {
|
||||||
protected abstract readonly regex: RegExp;
|
protected abstract readonly regex: RegExp;
|
||||||
@@ -21,7 +21,7 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
const matches = code.matchAll(this.regex);
|
const matches = code.matchAll(this.regex);
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const primitiveExpression = this.buildExpression(match);
|
const primitiveExpression = this.buildExpression(match);
|
||||||
const position = this.doOrRethrow(() => createPosition(match), 'invalid script position', code);
|
const position = this.doOrRethrow(() => createPositionFromRegexFullMatch(match), 'invalid script position', code);
|
||||||
const parameters = createParameters(primitiveExpression);
|
const parameters = createParameters(primitiveExpression);
|
||||||
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
const expression = new Expression(position, primitiveExpression.evaluator, parameters);
|
||||||
yield expression;
|
yield expression;
|
||||||
@@ -37,12 +37,6 @@ export abstract class RegexParser implements IExpressionParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
function createParameters(
|
function createParameters(
|
||||||
expression: IPrimitiveExpression,
|
expression: IPrimitiveExpression,
|
||||||
): FunctionParameterCollection {
|
): FunctionParameterCollection {
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ function hasLines(text: string) {
|
|||||||
*/
|
*/
|
||||||
function inlineComments(code: string): string {
|
function inlineComments(code: string): string {
|
||||||
const makeInlineComment = (comment: string) => {
|
const makeInlineComment = (comment: string) => {
|
||||||
const value = comment?.trim();
|
const value = comment.trim();
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return '<##>';
|
return '<##>';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,12 +15,6 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
private readonly pipes = new Map<string, IPipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
if (!pipes) {
|
|
||||||
throw new Error('missing pipes');
|
|
||||||
}
|
|
||||||
if (pipes.some((pipe) => !pipe)) {
|
|
||||||
throw new Error('missing pipe in list');
|
|
||||||
}
|
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
@@ -28,10 +22,11 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
|
|
||||||
public get(pipeName: string): IPipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
if (!this.pipes.has(pipeName)) {
|
const pipe = this.pipes.get(pipeName);
|
||||||
|
if (!pipe) {
|
||||||
throw new Error(`Unknown pipe: "${pipeName}"`);
|
throw new Error(`Unknown pipe: "${pipeName}"`);
|
||||||
}
|
}
|
||||||
return this.pipes.get(pipeName);
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: IPipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function
|
|||||||
import { IExpression } from '../Expression/IExpression';
|
import { IExpression } from '../Expression/IExpression';
|
||||||
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
import { ExpressionPosition } from '../Expression/ExpressionPosition';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
|
|
||||||
export class WithParser implements IExpressionParser {
|
export class WithParser implements IExpressionParser {
|
||||||
public findExpressions(code: string): IExpression[] {
|
public findExpressions(code: string): IExpression[] {
|
||||||
@@ -42,31 +43,25 @@ function parseAllWithExpressions(
|
|||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.Start,
|
type: WithStatementType.Start,
|
||||||
parameterName: match[1],
|
parameterName: match[1],
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
for (const match of input.matchAll(WithStatementEndRegEx)) {
|
||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.End,
|
type: WithStatementType.End,
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
for (const match of input.matchAll(ContextVariableWithPipelineRegEx)) {
|
||||||
expressions.push({
|
expressions.push({
|
||||||
type: WithStatementType.ContextVariable,
|
type: WithStatementType.ContextVariable,
|
||||||
position: createPosition(match),
|
position: createPositionFromRegexFullMatch(match),
|
||||||
pipeline: match[1],
|
pipeline: match[1],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return expressions;
|
return expressions;
|
||||||
}
|
}
|
||||||
|
|
||||||
function createPosition(match: RegExpMatchArray): ExpressionPosition {
|
|
||||||
const startPos = match.index;
|
|
||||||
const endPos = startPos + match[0].length;
|
|
||||||
return new ExpressionPosition(startPos, endPos);
|
|
||||||
}
|
|
||||||
|
|
||||||
class WithStatementBuilder {
|
class WithStatementBuilder {
|
||||||
private readonly contextVariables = new Array<{
|
private readonly contextVariables = new Array<{
|
||||||
readonly positionInScope: ExpressionPosition;
|
readonly positionInScope: ExpressionPosition;
|
||||||
@@ -125,7 +120,7 @@ class WithStatementBuilder {
|
|||||||
|
|
||||||
private substituteContextVariables(
|
private substituteContextVariables(
|
||||||
scope: string,
|
scope: string,
|
||||||
substituter: (pipeline: string) => string,
|
substituter: (pipeline?: string) => string,
|
||||||
): string {
|
): string {
|
||||||
if (!this.contextVariables.length) {
|
if (!this.contextVariables.length) {
|
||||||
return scope;
|
return scope;
|
||||||
@@ -157,7 +152,7 @@ function parseWithExpressions(input: string): IExpression[] {
|
|||||||
.sort((a, b) => b.position.start - a.position.start);
|
.sort((a, b) => b.position.start - a.position.start);
|
||||||
const expressions = new Array<IExpression>();
|
const expressions = new Array<IExpression>();
|
||||||
const builders = new Array<WithStatementBuilder>();
|
const builders = new Array<WithStatementBuilder>();
|
||||||
const throwWithContext = (message: string) => {
|
const throwWithContext = (message: string): never => {
|
||||||
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
throw new Error(`${message}\n${buildErrorContext(input, allStatements)}}`);
|
||||||
};
|
};
|
||||||
while (sortedStatements.length > 0) {
|
while (sortedStatements.length > 0) {
|
||||||
@@ -178,12 +173,15 @@ function parseWithExpressions(input: string): IExpression[] {
|
|||||||
}
|
}
|
||||||
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
builders[builders.length - 1].addContextVariable(statement.position, statement.pipeline);
|
||||||
break;
|
break;
|
||||||
case WithStatementType.End:
|
case WithStatementType.End: {
|
||||||
if (builders.length === 0) {
|
const builder = builders.pop();
|
||||||
|
if (!builder) {
|
||||||
throwWithContext('Redundant `end` statement, missing `with`?');
|
throwWithContext('Redundant `end` statement, missing `with`?');
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
expressions.push(builders.pop().buildExpression(statement.position, input));
|
expressions.push(builder.buildExpression(statement.position, input));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (builders.length > 0) {
|
if (builders.length > 0) {
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
public addArgument(argument: IFunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
if (!argument) {
|
|
||||||
throw new Error('missing argument');
|
|
||||||
}
|
|
||||||
if (this.hasArgument(argument.parameterName)) {
|
if (this.hasArgument(argument.parameterName)) {
|
||||||
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
throw new Error(`argument value for parameter ${argument.parameterName} is already provided`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,18 +3,22 @@ import { CodeSegmentMerger } from './CodeSegmentMerger';
|
|||||||
|
|
||||||
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
||||||
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
public mergeCodeParts(codeSegments: readonly CompiledCode[]): CompiledCode {
|
||||||
if (!codeSegments?.length) {
|
if (!codeSegments.length) {
|
||||||
throw new Error('missing segments');
|
throw new Error('missing segments');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
revertCode: joinCodeParts(codeSegments.map((f) => f.revertCode)),
|
revertCode: joinCodeParts(
|
||||||
|
codeSegments
|
||||||
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function joinCodeParts(codeSegments: readonly string[]): string {
|
function joinCodeParts(codeSegments: readonly string[]): string {
|
||||||
return codeSegments
|
return codeSegments
|
||||||
.filter((segment) => segment?.length > 0)
|
.filter((segment) => segment.length > 0)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,7 @@ export class FunctionCallSequenceCompiler implements FunctionCallCompiler {
|
|||||||
calls: readonly FunctionCall[],
|
calls: readonly FunctionCall[],
|
||||||
functions: ISharedFunctionCollection,
|
functions: ISharedFunctionCollection,
|
||||||
): CompiledCode {
|
): CompiledCode {
|
||||||
if (!functions) { throw new Error('missing functions'); }
|
if (!calls.length) { throw new Error('missing calls'); }
|
||||||
if (!calls?.length) { throw new Error('missing calls'); }
|
|
||||||
if (calls.some((f) => !f)) { throw new Error('missing function call'); }
|
|
||||||
const context: FunctionCallCompilationContext = {
|
const context: FunctionCallCompilationContext = {
|
||||||
allFunctions: functions,
|
allFunctions: functions,
|
||||||
rootCallSequence: calls,
|
rootCallSequence: calls,
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
import { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
import { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
|
||||||
@@ -12,19 +12,33 @@ export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.code !== undefined;
|
return func.body.type === FunctionBodyType.Code;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
calledFunction: ISharedFunction,
|
calledFunction: ISharedFunction,
|
||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
|
if (calledFunction.body.type !== FunctionBodyType.Code) {
|
||||||
|
throw new Error([
|
||||||
|
'Unexpected function body type.',
|
||||||
|
`\tExpected: "${FunctionBodyType[FunctionBodyType.Code]}"`,
|
||||||
|
`\tActual: "${FunctionBodyType[calledFunction.body.type]}"`,
|
||||||
|
'Function:',
|
||||||
|
`\t${JSON.stringify(callToFunction)}`,
|
||||||
|
].join('\n'));
|
||||||
|
}
|
||||||
const { code } = calledFunction.body;
|
const { code } = calledFunction.body;
|
||||||
const { args } = callToFunction;
|
const { args } = callToFunction;
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
code: this.expressionsCompiler.compileExpressions(code.execute, args),
|
||||||
revertCode: this.expressionsCompiler.compileExpressions(code.revert, args),
|
revertCode: (() => {
|
||||||
|
if (!code.revert) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return this.expressionsCompiler.compileExpressions(code.revert, args);
|
||||||
|
})(),
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
import { CallFunctionBody, FunctionBodyType, ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
import { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
import { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode';
|
||||||
@@ -13,7 +13,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(func: ISharedFunction): boolean {
|
public canCompile(func: ISharedFunction): boolean {
|
||||||
return func.body.calls !== undefined;
|
return func.body.type === FunctionBodyType.Calls;
|
||||||
}
|
}
|
||||||
|
|
||||||
public compileFunction(
|
public compileFunction(
|
||||||
@@ -21,7 +21,7 @@ export class NestedFunctionCallCompiler implements SingleCallCompilerStrategy {
|
|||||||
callToFunction: FunctionCall,
|
callToFunction: FunctionCall,
|
||||||
context: FunctionCallCompilationContext,
|
context: FunctionCallCompilationContext,
|
||||||
): CompiledCode[] {
|
): CompiledCode[] {
|
||||||
const nestedCalls = calledFunction.body.calls;
|
const nestedCalls = (calledFunction.body as CallFunctionBody).calls;
|
||||||
return nestedCalls.map((nestedCall) => {
|
return nestedCalls.map((nestedCall) => {
|
||||||
try {
|
try {
|
||||||
const compiledParentCall = this.argumentCompiler
|
const compiledParentCall = this.argumentCompiler
|
||||||
|
|||||||
@@ -5,9 +5,6 @@ import { FunctionCallArgument } from './Argument/FunctionCallArgument';
|
|||||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
||||||
|
|
||||||
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
export function parseFunctionCalls(calls: FunctionCallsData): FunctionCall[] {
|
||||||
if (calls === undefined) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const sequence = getCallSequence(calls);
|
const sequence = getCallSequence(calls);
|
||||||
return sequence.map((call) => parseFunctionCall(call));
|
return sequence.map((call) => parseFunctionCall(call));
|
||||||
}
|
}
|
||||||
@@ -19,22 +16,21 @@ function getCallSequence(calls: FunctionCallsData): FunctionCallData[] {
|
|||||||
if (calls instanceof Array) {
|
if (calls instanceof Array) {
|
||||||
return calls as FunctionCallData[];
|
return calls as FunctionCallData[];
|
||||||
}
|
}
|
||||||
return [calls as FunctionCallData];
|
const singleCall = calls;
|
||||||
|
return [singleCall];
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
function parseFunctionCall(call: FunctionCallData): FunctionCall {
|
||||||
if (!call) {
|
|
||||||
throw new Error('missing call data');
|
|
||||||
}
|
|
||||||
const callArgs = parseArgs(call.parameters);
|
const callArgs = parseArgs(call.parameters);
|
||||||
return new ParsedFunctionCall(call.function, callArgs);
|
return new ParsedFunctionCall(call.function, callArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseArgs(
|
function parseArgs(
|
||||||
parameters: FunctionCallParametersData,
|
parameters: FunctionCallParametersData | undefined,
|
||||||
): FunctionCallArgumentCollection {
|
): FunctionCallArgumentCollection {
|
||||||
return Object.keys(parameters || {})
|
const parametersMap = parameters ?? {};
|
||||||
.map((parameterName) => new FunctionCallArgument(parameterName, parameters[parameterName]))
|
return Object.keys(parametersMap)
|
||||||
|
.map((parameterName) => new FunctionCallArgument(parameterName, parametersMap[parameterName]))
|
||||||
.reduce((args, arg) => {
|
.reduce((args, arg) => {
|
||||||
args.addArgument(arg);
|
args.addArgument(arg);
|
||||||
return args;
|
return args;
|
||||||
|
|||||||
@@ -9,8 +9,5 @@ export class ParsedFunctionCall implements FunctionCall {
|
|||||||
if (!functionName) {
|
if (!functionName) {
|
||||||
throw new Error('missing function name in function call');
|
throw new Error('missing function name in function call');
|
||||||
}
|
}
|
||||||
if (!args) {
|
|
||||||
throw new Error('missing args');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,15 +4,21 @@ import { FunctionCall } from './Call/FunctionCall';
|
|||||||
export interface ISharedFunction {
|
export interface ISharedFunction {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly parameters: IReadOnlyFunctionParameterCollection;
|
readonly parameters: IReadOnlyFunctionParameterCollection;
|
||||||
readonly body: ISharedFunctionBody;
|
readonly body: SharedFunctionBody;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ISharedFunctionBody {
|
export interface CallFunctionBody {
|
||||||
readonly type: FunctionBodyType;
|
readonly type: FunctionBodyType.Calls,
|
||||||
readonly code: IFunctionCode | undefined;
|
readonly calls: readonly FunctionCall[],
|
||||||
readonly calls: readonly FunctionCall[] | undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CodeFunctionBody {
|
||||||
|
readonly type: FunctionBodyType.Code;
|
||||||
|
readonly code: IFunctionCode,
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SharedFunctionBody = CallFunctionBody | CodeFunctionBody;
|
||||||
|
|
||||||
export enum FunctionBodyType {
|
export enum FunctionBodyType {
|
||||||
Code,
|
Code,
|
||||||
Calls,
|
Calls,
|
||||||
|
|||||||
@@ -18,9 +18,6 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ensureValidParameter(parameter: IFunctionParameter) {
|
private ensureValidParameter(parameter: IFunctionParameter) {
|
||||||
if (!parameter) {
|
|
||||||
throw new Error('missing parameter');
|
|
||||||
}
|
|
||||||
if (this.includesName(parameter.name)) {
|
if (this.includesName(parameter.name)) {
|
||||||
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
throw new Error(`duplicate parameter name: "${parameter.name}"`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { FunctionCall } from './Call/FunctionCall';
|
import { FunctionCall } from './Call/FunctionCall';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
FunctionBodyType, IFunctionCode, ISharedFunction, ISharedFunctionBody,
|
FunctionBodyType, IFunctionCode, ISharedFunction, SharedFunctionBody,
|
||||||
} from './ISharedFunction';
|
} from './ISharedFunction';
|
||||||
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
import { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
@@ -10,7 +10,7 @@ export function createCallerFunction(
|
|||||||
parameters: IReadOnlyFunctionParameterCollection,
|
parameters: IReadOnlyFunctionParameterCollection,
|
||||||
callSequence: readonly FunctionCall[],
|
callSequence: readonly FunctionCall[],
|
||||||
): ISharedFunction {
|
): ISharedFunction {
|
||||||
if (!callSequence || !callSequence.length) {
|
if (!callSequence.length) {
|
||||||
throw new Error(`missing call sequence in function "${name}"`);
|
throw new Error(`missing call sequence in function "${name}"`);
|
||||||
}
|
}
|
||||||
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
return new SharedFunction(name, parameters, callSequence, FunctionBodyType.Calls);
|
||||||
@@ -33,7 +33,7 @@ export function createFunctionWithInlineCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class SharedFunction implements ISharedFunction {
|
class SharedFunction implements ISharedFunction {
|
||||||
public readonly body: ISharedFunctionBody;
|
public readonly body: SharedFunctionBody;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
@@ -42,11 +42,22 @@ class SharedFunction implements ISharedFunction {
|
|||||||
bodyType: FunctionBodyType,
|
bodyType: FunctionBodyType,
|
||||||
) {
|
) {
|
||||||
if (!name) { throw new Error('missing function name'); }
|
if (!name) { throw new Error('missing function name'); }
|
||||||
if (!parameters) { throw new Error('missing parameters'); }
|
|
||||||
this.body = {
|
switch (bodyType) {
|
||||||
type: bodyType,
|
case FunctionBodyType.Code:
|
||||||
code: bodyType === FunctionBodyType.Code ? content as IFunctionCode : undefined,
|
this.body = {
|
||||||
calls: bodyType === FunctionBodyType.Calls ? content as readonly FunctionCall[] : undefined,
|
type: FunctionBodyType.Code,
|
||||||
};
|
code: content as IFunctionCode,
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
case FunctionBodyType.Calls:
|
||||||
|
this.body = {
|
||||||
|
type: FunctionBodyType.Calls,
|
||||||
|
calls: content as readonly FunctionCall[],
|
||||||
|
};
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error(`unknown body type: ${FunctionBodyType[bodyType]}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ export class SharedFunctionCollection implements ISharedFunctionCollection {
|
|||||||
private readonly functionsByName = new Map<string, ISharedFunction>();
|
private readonly functionsByName = new Map<string, ISharedFunction>();
|
||||||
|
|
||||||
public addFunction(func: ISharedFunction): void {
|
public addFunction(func: ISharedFunction): void {
|
||||||
if (!func) { throw new Error('missing function'); }
|
|
||||||
if (this.has(func.name)) {
|
if (this.has(func.name)) {
|
||||||
throw new Error(`function with name ${func.name} already exists`);
|
throw new Error(`function with name ${func.name} already exists`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import type { FunctionData, InstructionHolder } from '@/application/collections/';
|
import type {
|
||||||
|
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, CallInstruction,
|
||||||
|
} from '@/application/collections/';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
@@ -23,9 +25,8 @@ export class SharedFunctionsParser implements ISharedFunctionsParser {
|
|||||||
functions: readonly FunctionData[],
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
): ISharedFunctionCollection {
|
): ISharedFunctionCollection {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
const collection = new SharedFunctionCollection();
|
const collection = new SharedFunctionCollection();
|
||||||
if (!functions || !functions.length) {
|
if (!functions.length) {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
ensureValidFunctions(functions);
|
ensureValidFunctions(functions);
|
||||||
@@ -55,16 +56,18 @@ function parseFunction(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(
|
function validateCode(
|
||||||
data: FunctionData,
|
data: CodeFunctionData,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
validator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
): void {
|
): void {
|
||||||
[data.code, data.revertCode].forEach(
|
[data.code, data.revertCode]
|
||||||
(code) => validator.throwIfInvalid(
|
.filter((code): code is string => Boolean(code))
|
||||||
code,
|
.forEach(
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
(code) => validator.throwIfInvalid(
|
||||||
),
|
code,
|
||||||
);
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollection {
|
||||||
@@ -85,19 +88,18 @@ function parseParameters(data: FunctionData): IReadOnlyFunctionParameterCollecti
|
|||||||
}, new FunctionParameterCollection());
|
}, new FunctionParameterCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCode(data: FunctionData): boolean {
|
function hasCode(data: FunctionData): data is CodeFunctionData {
|
||||||
return Boolean(data.code);
|
return (data as CodeInstruction).code !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function hasCall(data: FunctionData): boolean {
|
function hasCall(data: FunctionData): data is CallFunctionData {
|
||||||
return Boolean(data.call);
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
function ensureValidFunctions(functions: readonly FunctionData[]) {
|
||||||
ensureNoUndefinedItem(functions);
|
|
||||||
ensureNoDuplicatesInFunctionNames(functions);
|
ensureNoDuplicatesInFunctionNames(functions);
|
||||||
ensureNoDuplicateCode(functions);
|
|
||||||
ensureEitherCallOrCodeIsDefined(functions);
|
ensureEitherCallOrCodeIsDefined(functions);
|
||||||
|
ensureNoDuplicateCode(functions);
|
||||||
ensureExpectedParametersType(functions);
|
ensureExpectedParametersType(functions);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ function printList(list: readonly string[]): string {
|
|||||||
return `"${list.join('","')}"`;
|
return `"${list.join('","')}"`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureEitherCallOrCodeIsDefined(holders: readonly InstructionHolder[]) {
|
function ensureEitherCallOrCodeIsDefined(holders: readonly FunctionData[]) {
|
||||||
// Ensure functions do not define both call and code
|
// Ensure functions do not define both call and code
|
||||||
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
const withBothCallAndCode = holders.filter((holder) => hasCode(holder) && hasCall(holder));
|
||||||
if (withBothCallAndCode.length) {
|
if (withBothCallAndCode.length) {
|
||||||
@@ -132,7 +134,7 @@ function isArrayOfObjects(value: unknown): boolean {
|
|||||||
&& value.every((item) => typeof item === 'object');
|
&& value.every((item) => typeof item === 'object');
|
||||||
}
|
}
|
||||||
|
|
||||||
function printNames(holders: readonly InstructionHolder[]) {
|
function printNames(holders: readonly FunctionData[]) {
|
||||||
return printList(holders.map((holder) => holder.name));
|
return printList(holders.map((holder) => holder.name));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,22 +146,19 @@ function ensureNoDuplicatesInFunctionNames(functions: readonly FunctionData[]) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureNoUndefinedItem(functions: readonly FunctionData[]) {
|
|
||||||
if (functions.some((func) => !func)) {
|
|
||||||
throw new Error('some functions are undefined');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
function ensureNoDuplicateCode(functions: readonly FunctionData[]) {
|
||||||
const duplicateCodes = getDuplicates(functions
|
const callFunctions = functions
|
||||||
|
.filter((func) => hasCode(func))
|
||||||
|
.map((func) => func as CodeFunctionData);
|
||||||
|
const duplicateCodes = getDuplicates(callFunctions
|
||||||
.map((func) => func.code)
|
.map((func) => func.code)
|
||||||
.filter((code) => code));
|
.filter((code) => code));
|
||||||
if (duplicateCodes.length > 0) {
|
if (duplicateCodes.length > 0) {
|
||||||
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
throw new Error(`duplicate "code" in functions: ${printList(duplicateCodes)}`);
|
||||||
}
|
}
|
||||||
const duplicateRevertCodes = getDuplicates(functions
|
const duplicateRevertCodes = getDuplicates(callFunctions
|
||||||
.filter((func) => func.revertCode)
|
.map((func) => func.revertCode)
|
||||||
.map((func) => func.revertCode));
|
.filter((code): code is string => Boolean(code)));
|
||||||
if (duplicateRevertCodes.length > 0) {
|
if (duplicateRevertCodes.length > 0) {
|
||||||
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
throw new Error(`duplicate "revertCode" in functions: ${printList(duplicateRevertCodes)}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FunctionData, ScriptData } from '@/application/collections/';
|
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
||||||
import { IScriptCode } from '@/domain/IScriptCode';
|
import { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import { ScriptCode } from '@/domain/ScriptCode';
|
import { ScriptCode } from '@/domain/ScriptCode';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
@@ -18,27 +18,24 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
private readonly functions: ISharedFunctionCollection;
|
private readonly functions: ISharedFunctionCollection;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
functions: readonly FunctionData[] | undefined,
|
functions: readonly FunctionData[],
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
sharedFunctionsParser: ISharedFunctionsParser = SharedFunctionsParser.instance,
|
||||||
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
private readonly callCompiler: FunctionCallCompiler = FunctionCallSequenceCompiler.instance,
|
||||||
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
private readonly codeValidator: ICodeValidator = CodeValidator.instance,
|
||||||
) {
|
) {
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
this.functions = sharedFunctionsParser.parseFunctions(functions, syntax);
|
||||||
}
|
}
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
public canCompile(script: ScriptData): boolean {
|
||||||
if (!script) { throw new Error('missing script'); }
|
return hasCall(script);
|
||||||
if (!script.call) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public compile(script: ScriptData): IScriptCode {
|
public compile(script: ScriptData): IScriptCode {
|
||||||
if (!script) { throw new Error('missing script'); }
|
|
||||||
try {
|
try {
|
||||||
|
if (!hasCall(script)) {
|
||||||
|
throw new Error('Script does include any calls.');
|
||||||
|
}
|
||||||
const calls = parseFunctionCalls(script.call);
|
const calls = parseFunctionCalls(script.call);
|
||||||
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
const compiledCode = this.callCompiler.compileFunctionCalls(calls, this.functions);
|
||||||
validateCompiledCode(compiledCode, this.codeValidator);
|
validateCompiledCode(compiledCode, this.codeValidator);
|
||||||
@@ -53,7 +50,17 @@ export class ScriptCompiler implements IScriptCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
||||||
[compiledCode.code, compiledCode.revertCode].forEach(
|
[compiledCode.code, compiledCode.revertCode]
|
||||||
(code) => validator.throwIfInvalid(code, [new NoEmptyLines()]),
|
.filter((code): code is string => Boolean(code))
|
||||||
);
|
.map((code) => code as string)
|
||||||
|
.forEach(
|
||||||
|
(code) => validator.throwIfInvalid(
|
||||||
|
code,
|
||||||
|
[new NoEmptyLines()],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
||||||
|
return (data as CallInstruction).call !== undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ScriptData } from '@/application/collections/';
|
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
||||||
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines';
|
||||||
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
import { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax';
|
||||||
import { Script } from '@/domain/Script';
|
import { Script } from '@/domain/Script';
|
||||||
@@ -14,7 +14,6 @@ import { ICategoryCollectionParseContext } from './ICategoryCollectionParseConte
|
|||||||
import { CodeValidator } from './Validation/CodeValidator';
|
import { CodeValidator } from './Validation/CodeValidator';
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
||||||
|
|
||||||
// eslint-disable-next-line consistent-return
|
|
||||||
export function parseScript(
|
export function parseScript(
|
||||||
data: ScriptData,
|
data: ScriptData,
|
||||||
context: ICategoryCollectionParseContext,
|
context: ICategoryCollectionParseContext,
|
||||||
@@ -24,7 +23,6 @@ export function parseScript(
|
|||||||
): Script {
|
): Script {
|
||||||
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
const validator = new NodeValidator({ type: NodeType.Script, selfNode: data });
|
||||||
validateScript(data, validator);
|
validateScript(data, validator);
|
||||||
if (!context) { throw new Error('missing context'); }
|
|
||||||
try {
|
try {
|
||||||
const script = scriptFactory(
|
const script = scriptFactory(
|
||||||
/* name: */ data.name,
|
/* name: */ data.name,
|
||||||
@@ -34,12 +32,12 @@ export function parseScript(
|
|||||||
);
|
);
|
||||||
return script;
|
return script;
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
validator.throw(err.message);
|
return validator.throw(err.message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseLevel(
|
function parseLevel(
|
||||||
level: string,
|
level: string | undefined,
|
||||||
parser: IEnumParser<RecommendationLevel>,
|
parser: IEnumParser<RecommendationLevel>,
|
||||||
): RecommendationLevel | undefined {
|
): RecommendationLevel | undefined {
|
||||||
if (!level) {
|
if (!level) {
|
||||||
@@ -56,39 +54,45 @@ function parseCode(
|
|||||||
if (context.compiler.canCompile(script)) {
|
if (context.compiler.canCompile(script)) {
|
||||||
return context.compiler.compile(script);
|
return context.compiler.compile(script);
|
||||||
}
|
}
|
||||||
const code = new ScriptCode(script.code, script.revertCode);
|
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
||||||
|
const code = new ScriptCode(codeScript.code, codeScript.revertCode);
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
validateHardcodedCodeWithoutCalls(code, codeValidator, context.syntax);
|
||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
function validateHardcodedCodeWithoutCalls(
|
||||||
scriptCode: ScriptCode,
|
scriptCode: ScriptCode,
|
||||||
codeValidator: ICodeValidator,
|
validator: ICodeValidator,
|
||||||
syntax: ILanguageSyntax,
|
syntax: ILanguageSyntax,
|
||||||
) {
|
) {
|
||||||
[scriptCode.execute, scriptCode.revert].forEach(
|
[scriptCode.execute, scriptCode.revert]
|
||||||
(code) => codeValidator.throwIfInvalid(
|
.filter((code): code is string => Boolean(code))
|
||||||
code,
|
.forEach(
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
(code) => validator.throwIfInvalid(
|
||||||
),
|
code,
|
||||||
);
|
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateScript(script: ScriptData, validator: NodeValidator) {
|
function validateScript(
|
||||||
|
script: ScriptData,
|
||||||
|
validator: NodeValidator,
|
||||||
|
): asserts script is NonNullable<ScriptData> {
|
||||||
validator
|
validator
|
||||||
.assertDefined(script)
|
.assertDefined(script)
|
||||||
.assertValidName(script.name)
|
.assertValidName(script.name)
|
||||||
.assert(
|
.assert(
|
||||||
() => Boolean(script.code || script.call),
|
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
||||||
'Must define either "call" or "code".',
|
'Neither "call" or "code" is defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.code && script.call),
|
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
||||||
'Cannot define both "call" and "code".',
|
'Both "call" and "code" are defined.',
|
||||||
)
|
)
|
||||||
.assert(
|
.assert(
|
||||||
() => !(script.revertCode && script.call),
|
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
||||||
'Cannot define "revertCode" if "call" is defined.',
|
'Both "call" and "revertCode" are defined.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export class CodeValidator implements ICodeValidator {
|
|||||||
code: string,
|
code: string,
|
||||||
rules: readonly ICodeValidationRule[],
|
rules: readonly ICodeValidationRule[],
|
||||||
): void {
|
): void {
|
||||||
if (!rules || rules.length === 0) { throw new Error('missing rules'); }
|
if (rules.length === 0) { throw new Error('missing rules'); }
|
||||||
if (!code) {
|
if (!code) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,7 @@ import { ICodeLine } from '../ICodeLine';
|
|||||||
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
import { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';
|
||||||
|
|
||||||
export class NoDuplicatedLines implements ICodeValidationRule {
|
export class NoDuplicatedLines implements ICodeValidationRule {
|
||||||
constructor(private readonly syntax: ILanguageSyntax) {
|
constructor(private readonly syntax: ILanguageSyntax) { }
|
||||||
if (!syntax) { throw new Error('missing syntax'); }
|
|
||||||
}
|
|
||||||
|
|
||||||
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
public analyze(lines: readonly ICodeLine[]): IInvalidCodeLine[] {
|
||||||
return lines
|
return lines
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ export class CodeSubstituter implements ICodeSubstituter {
|
|||||||
|
|
||||||
public substitute(code: string, info: IProjectInformation): string {
|
public substitute(code: string, info: IProjectInformation): string {
|
||||||
if (!code) { throw new Error('missing code'); }
|
if (!code) { throw new Error('missing code'); }
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
const args = new FunctionCallArgumentCollection();
|
const args = new FunctionCallArgumentCollection();
|
||||||
const substitute = (name: string, value: string) => args
|
const substitute = (name: string, value: string) => args
|
||||||
.addArgument(new FunctionCallArgument(name, value));
|
.addArgument(new FunctionCallArgument(name, value));
|
||||||
|
|||||||
@@ -18,8 +18,6 @@ export class ScriptingDefinitionParser {
|
|||||||
definition: ScriptingDefinitionData,
|
definition: ScriptingDefinitionData,
|
||||||
info: IProjectInformation,
|
info: IProjectInformation,
|
||||||
): IScriptingDefinition {
|
): IScriptingDefinition {
|
||||||
if (!info) { throw new Error('missing info'); }
|
|
||||||
if (!definition) { throw new Error('missing definition'); }
|
|
||||||
const language = this.languageParser.parseEnum(definition.language, 'language');
|
const language = this.languageParser.parseEnum(definition.language, 'language');
|
||||||
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
const startCode = this.codeSubstituter.substitute(definition.startCode, info);
|
||||||
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
const endCode = this.codeSubstituter.substitute(definition.endCode, info);
|
||||||
|
|||||||
17
src/application/Repository/Repository.ts
Normal file
17
src/application/Repository/Repository.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
|
|
||||||
|
export interface ReadonlyRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
|
readonly length: number;
|
||||||
|
getItems(predicate?: (entity: TEntity) => boolean): readonly TEntity[];
|
||||||
|
getById(id: TKey): TEntity;
|
||||||
|
exists(id: TKey): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MutableRepository<TKey, TEntity extends IEntity<TKey>> {
|
||||||
|
addItem(item: TEntity): void;
|
||||||
|
addOrUpdateItem(item: TEntity): void;
|
||||||
|
removeItem(id: TKey): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Repository<TKey, TEntity extends IEntity<TKey>>
|
||||||
|
extends ReadonlyRepository<TKey, TEntity>, MutableRepository<TKey, TEntity> { }
|
||||||
37
src/application/collections/collection.yaml.d.ts
vendored
37
src/application/collections/collection.yaml.d.ts
vendored
@@ -12,29 +12,38 @@ declare module '@/application/collections/*' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export type CategoryOrScriptData = CategoryData | ScriptData;
|
export type CategoryOrScriptData = CategoryData | ScriptData;
|
||||||
export type DocumentationData = ReadonlyArray<string> | string;
|
export type DocumentationData = ReadonlyArray<string> | string | undefined;
|
||||||
|
|
||||||
export interface DocumentableData {
|
export interface DocumentableData {
|
||||||
readonly docs?: DocumentationData;
|
readonly docs?: DocumentationData;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface InstructionHolder {
|
export interface CodeInstruction {
|
||||||
readonly name: string;
|
readonly code: string;
|
||||||
|
|
||||||
readonly code?: string;
|
|
||||||
readonly revertCode?: string;
|
readonly revertCode?: string;
|
||||||
|
|
||||||
readonly call?: FunctionCallsData;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CallInstruction {
|
||||||
|
readonly call: FunctionCallsData;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type InstructionHolder = CodeInstruction | CallInstruction;
|
||||||
|
|
||||||
export interface ParameterDefinitionData {
|
export interface ParameterDefinitionData {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly optional?: boolean;
|
readonly optional?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface FunctionData extends InstructionHolder {
|
export type FunctionDefinition = {
|
||||||
|
readonly name: string;
|
||||||
readonly parameters?: readonly ParameterDefinitionData[];
|
readonly parameters?: readonly ParameterDefinitionData[];
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeFunctionData = FunctionDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallFunctionData = FunctionDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type FunctionData = CodeFunctionData | CallFunctionData;
|
||||||
|
|
||||||
export interface FunctionCallParametersData {
|
export interface FunctionCallParametersData {
|
||||||
readonly [index: string]: string;
|
readonly [index: string]: string;
|
||||||
@@ -47,10 +56,16 @@ declare module '@/application/collections/*' {
|
|||||||
|
|
||||||
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
export type FunctionCallsData = readonly FunctionCallData[] | FunctionCallData | undefined;
|
||||||
|
|
||||||
export interface ScriptData extends InstructionHolder, DocumentableData {
|
export type ScriptDefinition = DocumentableData & {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly recommend?: string;
|
readonly recommend?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
export type CodeScriptData = ScriptDefinition & CodeInstruction;
|
||||||
|
|
||||||
|
export type CallScriptData = ScriptDefinition & CallInstruction;
|
||||||
|
|
||||||
|
export type ScriptData = CodeScriptData | CallScriptData;
|
||||||
|
|
||||||
export interface ScriptingDefinitionData {
|
export interface ScriptingDefinitionData {
|
||||||
readonly language: string;
|
readonly language: string;
|
||||||
|
|||||||
@@ -3739,7 +3739,7 @@ functions:
|
|||||||
- name: prefName
|
- name: prefName
|
||||||
- name: jsonValue
|
- name: jsonValue
|
||||||
docs: |-
|
docs: |-
|
||||||
This script either creates or updates the `user.js` file to set specific Mozilla Firefox preferences.
|
This script modifies the `user.js` file in Firefox profiles to set specific preferences.
|
||||||
|
|
||||||
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
The `user.js` file can be found in a Firefox profile folder [1] and its location depends on the type of installation:
|
||||||
|
|
||||||
@@ -3747,12 +3747,18 @@ functions:
|
|||||||
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
- Flatpak: `~/.var/app/org.mozilla.firefox/.mozilla/firefox/<profile-name>/user.js`
|
||||||
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
- Snap: `~/snap/firefox/common/.mozilla/firefox/<profile-name>/user.js`
|
||||||
|
|
||||||
While the `user.js` file is optional [2], if it's present, the Firefox application will prioritize its settings over
|
While the `user.js` file is optional [2], if it's present, the Firefox will prioritize its settings over
|
||||||
those in `prefs.js` upon startup [1][2]. To prevent potential profile corruption, Mozilla advises against editing
|
those in `prefs.js` upon startup [1] [2]. It's recommended not to directly edit `prefs.js` to avoid profile corruption [2].
|
||||||
`prefs.js` directly [2].
|
|
||||||
|
When `user.js` is modified or deleted, corresponding changes in `prefs.js` are necessary for reversion, as Firefox
|
||||||
|
doesn't automatically revert these changes [3].
|
||||||
|
|
||||||
|
This script safely modifies `user.js` and ensures changes are reflected in `prefs.js` during reversion, addressing
|
||||||
|
issues with preference persistence [3].
|
||||||
|
|
||||||
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
[1]: https://web.archive.org/web/20230811005205/https://kb.mozillazine.org/User.js_file "User.js file - MozillaZine Knowledge Base"
|
||||||
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
[2]: https://web.archive.org/web/20221029211757/https://kb.mozillazine.org/Prefs.js_file "Prefs.js file - MozillaZine Knowledge Base"
|
||||||
|
[3]: https://github.com/undergroundwires/privacy.sexy/issues/282 "[BUG]: Reverting Firefox settings do not work on Linux · Issue #282 · undergroundwires/privacy.sexy | github.com"
|
||||||
code: |-
|
code: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
@@ -3792,12 +3798,16 @@ functions:
|
|||||||
if [ "$total_profiles_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo 'No profile folders are found, no changes are made.'
|
echo 'No profile folders are found, no changes are made.'
|
||||||
else
|
else
|
||||||
echo "Preferences verified in $total_profiles_found profiles."
|
echo "Successfully verified preferences in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
revertCode: |-
|
revertCode: |-
|
||||||
pref_name='{{ $prefName }}'
|
pref_name='{{ $prefName }}'
|
||||||
pref_value='{{ $jsonValue }}'
|
pref_value='{{ $jsonValue }}'
|
||||||
echo "Reverting preference: \"$pref_name\" to its default."
|
echo "Reverting preference: \"$pref_name\" to its default."
|
||||||
|
if command -v 'ps' &> /dev/null && ps aux | grep -i "[f]irefox" > /dev/null; then
|
||||||
|
>&2 echo -e "\e[33mWarning: Firefox is currently running. Please close Firefox before executing the revert script to ensure changes are applied effectively.\e[0m"
|
||||||
|
fi
|
||||||
|
declare -a files_to_modify=('prefs.js' 'user.js')
|
||||||
declare -a profile_paths=(
|
declare -a profile_paths=(
|
||||||
~/.mozilla/firefox/*/
|
~/.mozilla/firefox/*/
|
||||||
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
~/.var/app/org.mozilla.firefox/.mozilla/firefox/*/
|
||||||
@@ -3805,31 +3815,39 @@ functions:
|
|||||||
)
|
)
|
||||||
declare -i total_profiles_found=0
|
declare -i total_profiles_found=0
|
||||||
for profile_dir in "${profile_paths[@]}"; do
|
for profile_dir in "${profile_paths[@]}"; do
|
||||||
user_js_file="${profile_dir}user.js"
|
if [ ! -d "$profile_dir" ]; then
|
||||||
if [ ! -f "$user_js_file" ]; then
|
|
||||||
continue
|
continue
|
||||||
fi
|
fi
|
||||||
((total_profiles_found++))
|
if [[ ! "$(basename "$profile_dir")" =~ ^[a-z0-9]{8}\..+ ]]; then
|
||||||
echo "$user_js_file:"
|
continue # Not a profile folder
|
||||||
pref_start="user_pref(\"$pref_name\","
|
|
||||||
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
|
||||||
if ! grep --quiet "^$pref_start" "${user_js_file}"; then
|
|
||||||
echo $'\t''Skipping, preference was not configured before.'
|
|
||||||
elif grep --quiet "^$pref_line$" "${user_js_file}"; then
|
|
||||||
sed --in-place "/^$pref_line/d" "$user_js_file"
|
|
||||||
echo $'\t''Successfully reverted preference to default.'
|
|
||||||
if ! grep --quiet '[^[:space:]]' "$user_js_file"; then
|
|
||||||
rm "$user_js_file"
|
|
||||||
echo $'\t''Removed user.js file as it became empty.'
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
|
||||||
fi
|
fi
|
||||||
|
((total_profiles_found++))
|
||||||
|
for file_to_modify in "${files_to_modify[@]}"; do
|
||||||
|
config_file_path="${profile_dir}${file_to_modify}"
|
||||||
|
if [ ! -f "$config_file_path" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
echo "$config_file_path:"
|
||||||
|
pref_start="user_pref(\"$pref_name\","
|
||||||
|
pref_line="user_pref(\"$pref_name\", $pref_value);"
|
||||||
|
if ! grep --quiet "^$pref_start" "${config_file_path}"; then
|
||||||
|
echo $'\t''Skipping, preference was not configured before.'
|
||||||
|
elif grep --quiet "^$pref_line$" "${config_file_path}"; then
|
||||||
|
sed --in-place "/^$pref_line/d" "$config_file_path"
|
||||||
|
echo $'\t''Successfully reverted preference to default.'
|
||||||
|
if ! grep --quiet '[^[:space:]]' "$config_file_path"; then
|
||||||
|
rm "$config_file_path"
|
||||||
|
echo $'\t'"Removed the file as it became empty."
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo $'\t''Skipping, the preference has value that is not configured by privacy.sexy.'
|
||||||
|
fi
|
||||||
|
done
|
||||||
done
|
done
|
||||||
if [ "$total_profiles_found" -eq 0 ]; then
|
if [ "$total_profiles_found" -eq 0 ]; then
|
||||||
echo 'No reversion was necessary.'
|
echo 'No reversion was necessary.'
|
||||||
else
|
else
|
||||||
echo "Preferences verified in $total_profiles_found profiles."
|
echo "Successfully verified preferences in $total_profiles_found profiles."
|
||||||
fi
|
fi
|
||||||
-
|
-
|
||||||
name: RenameFile
|
name: RenameFile
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,6 @@ export class Application implements IApplication {
|
|||||||
public info: IProjectInformation,
|
public info: IProjectInformation,
|
||||||
public collections: readonly ICategoryCollection[],
|
public collections: readonly ICategoryCollection[],
|
||||||
) {
|
) {
|
||||||
validateInformation(info);
|
|
||||||
validateCollections(collections);
|
validateCollections(collections);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -16,19 +15,17 @@ export class Application implements IApplication {
|
|||||||
return this.collections.map((collection) => collection.os);
|
return this.collections.map((collection) => collection.os);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined {
|
public getCollection(operatingSystem: OperatingSystem): ICategoryCollection {
|
||||||
return this.collections.find((collection) => collection.os === operatingSystem);
|
const collection = this.collections.find((c) => c.os === operatingSystem);
|
||||||
}
|
if (!collection) {
|
||||||
}
|
throw new Error(`Operating system "${OperatingSystem[operatingSystem]}" is not defined in application`);
|
||||||
|
}
|
||||||
function validateInformation(info: IProjectInformation) {
|
return collection;
|
||||||
if (!info) {
|
|
||||||
throw new Error('missing project information');
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateCollections(collections: readonly ICategoryCollection[]) {
|
function validateCollections(collections: readonly ICategoryCollection[]) {
|
||||||
if (!collections || !collections.length) {
|
if (!collections.length) {
|
||||||
throw new Error('missing collections');
|
throw new Error('missing collections');
|
||||||
}
|
}
|
||||||
if (collections.filter((c) => !c).length > 0) {
|
if (collections.filter((c) => !c).length > 0) {
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScript } from './IScript';
|
|||||||
import { ICategory } from './ICategory';
|
import { ICategory } from './ICategory';
|
||||||
|
|
||||||
export class Category extends BaseEntity<number> implements ICategory {
|
export class Category extends BaseEntity<number> implements ICategory {
|
||||||
private allSubScripts: ReadonlyArray<IScript> = undefined;
|
private allSubScripts?: ReadonlyArray<IScript> = undefined;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
id: number,
|
id: number,
|
||||||
public readonly name: string,
|
public readonly name: string,
|
||||||
public readonly docs: ReadonlyArray<string>,
|
public readonly docs: ReadonlyArray<string>,
|
||||||
public readonly subCategories?: ReadonlyArray<ICategory>,
|
public readonly subCategories: ReadonlyArray<ICategory>,
|
||||||
public readonly scripts?: ReadonlyArray<IScript>,
|
public readonly scripts: ReadonlyArray<IScript>,
|
||||||
) {
|
) {
|
||||||
super(id);
|
super(id);
|
||||||
validateCategory(this);
|
validateCategory(this);
|
||||||
@@ -39,10 +39,7 @@ function validateCategory(category: ICategory) {
|
|||||||
if (!category.name) {
|
if (!category.name) {
|
||||||
throw new Error('missing name');
|
throw new Error('missing name');
|
||||||
}
|
}
|
||||||
if (
|
if (category.subCategories.length === 0 && category.scripts.length === 0) {
|
||||||
(!category.subCategories || category.subCategories.length === 0)
|
|
||||||
&& (!category.scripts || category.scripts.length === 0)
|
|
||||||
) {
|
|
||||||
throw new Error('A category must have at least one sub-category or script');
|
throw new Error('A category must have at least one sub-category or script');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,6 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
public readonly actions: ReadonlyArray<ICategory>,
|
public readonly actions: ReadonlyArray<ICategory>,
|
||||||
public readonly scripting: IScriptingDefinition,
|
public readonly scripting: IScriptingDefinition,
|
||||||
) {
|
) {
|
||||||
if (!scripting) {
|
|
||||||
throw new Error('missing scripting definition');
|
|
||||||
}
|
|
||||||
this.queryable = makeQueryable(actions);
|
this.queryable = makeQueryable(actions);
|
||||||
assertInRange(os, OperatingSystem);
|
assertInRange(os, OperatingSystem);
|
||||||
ensureValid(this.queryable);
|
ensureValid(this.queryable);
|
||||||
@@ -29,17 +26,26 @@ export class CategoryCollection implements ICategoryCollection {
|
|||||||
ensureNoDuplicates(this.queryable.allScripts);
|
ensureNoDuplicates(this.queryable.allScripts);
|
||||||
}
|
}
|
||||||
|
|
||||||
public findCategory(categoryId: number): ICategory | undefined {
|
public getCategory(categoryId: number): ICategory {
|
||||||
return this.queryable.allCategories.find((category) => category.id === categoryId);
|
const category = this.queryable.allCategories.find((c) => c.id === categoryId);
|
||||||
|
if (!category) {
|
||||||
|
throw new Error(`Missing category with ID: "${categoryId}"`);
|
||||||
|
}
|
||||||
|
return category;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
|
||||||
assertInRange(level, RecommendationLevel);
|
assertInRange(level, RecommendationLevel);
|
||||||
return this.queryable.scriptsByLevel.get(level);
|
const scripts = this.queryable.scriptsByLevel.get(level);
|
||||||
|
return scripts ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
public findScript(scriptId: string): IScript | undefined {
|
public getScript(scriptId: string): IScript {
|
||||||
return this.queryable.allScripts.find((script) => script.id === scriptId);
|
const script = this.queryable.allScripts.find((s) => s.id === scriptId);
|
||||||
|
if (!script) {
|
||||||
|
throw new Error(`missing script: ${scriptId}`);
|
||||||
|
}
|
||||||
|
return script;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getAllScripts(): IScript[] {
|
public getAllScripts(): IScript[] {
|
||||||
@@ -78,13 +84,13 @@ function ensureValid(application: IQueryableCollection) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
function ensureValidCategories(allCategories: readonly ICategory[]) {
|
||||||
if (!allCategories || allCategories.length === 0) {
|
if (!allCategories.length) {
|
||||||
throw new Error('must consist of at least one category');
|
throw new Error('must consist of at least one category');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function ensureValidScripts(allScripts: readonly IScript[]) {
|
function ensureValidScripts(allScripts: readonly IScript[]) {
|
||||||
if (!allScripts || allScripts.length === 0) {
|
if (!allScripts.length) {
|
||||||
throw new Error('must consist of at least one script');
|
throw new Error('must consist of at least one script');
|
||||||
}
|
}
|
||||||
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
const missingRecommendationLevels = getEnumValues(RecommendationLevel)
|
||||||
|
|||||||
@@ -7,5 +7,5 @@ export interface IApplication {
|
|||||||
readonly collections: readonly ICategoryCollection[];
|
readonly collections: readonly ICategoryCollection[];
|
||||||
|
|
||||||
getSupportedOsList(): OperatingSystem[];
|
getSupportedOsList(): OperatingSystem[];
|
||||||
getCollection(operatingSystem: OperatingSystem): ICategoryCollection | undefined;
|
getCollection(operatingSystem: OperatingSystem): ICategoryCollection;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { IDocumentable } from './IDocumentable';
|
|||||||
export interface ICategory extends IEntity<number>, IDocumentable {
|
export interface ICategory extends IEntity<number>, IDocumentable {
|
||||||
readonly id: number;
|
readonly id: number;
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
readonly subCategories?: ReadonlyArray<ICategory>;
|
readonly subCategories: ReadonlyArray<ICategory>;
|
||||||
readonly scripts?: ReadonlyArray<IScript>;
|
readonly scripts: ReadonlyArray<IScript>;
|
||||||
includes(script: IScript): boolean;
|
includes(script: IScript): boolean;
|
||||||
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
getAllScriptsRecursively(): ReadonlyArray<IScript>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,8 @@ export interface ICategoryCollection {
|
|||||||
readonly actions: ReadonlyArray<ICategory>;
|
readonly actions: ReadonlyArray<ICategory>;
|
||||||
|
|
||||||
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
|
||||||
findCategory(categoryId: number): ICategory | undefined;
|
getCategory(categoryId: number): ICategory;
|
||||||
findScript(scriptId: string): IScript | undefined;
|
getScript(scriptId: string): IScript;
|
||||||
getAllScripts(): ReadonlyArray<IScript>;
|
getAllScripts(): ReadonlyArray<IScript>;
|
||||||
getAllCategories(): ReadonlyArray<ICategory>;
|
getAllCategories(): ReadonlyArray<ICategory>;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface IScriptCode {
|
export interface IScriptCode {
|
||||||
readonly execute: string;
|
readonly execute: string;
|
||||||
readonly revert: string;
|
readonly revert?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,9 +16,6 @@ export class ProjectInformation implements IProjectInformation {
|
|||||||
if (!name) {
|
if (!name) {
|
||||||
throw new Error('name is undefined');
|
throw new Error('name is undefined');
|
||||||
}
|
}
|
||||||
if (!version) {
|
|
||||||
throw new Error('undefined version');
|
|
||||||
}
|
|
||||||
if (!slogan) {
|
if (!slogan) {
|
||||||
throw new Error('undefined slogan');
|
throw new Error('undefined slogan');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,9 +11,6 @@ export class Script extends BaseEntity<string> implements IScript {
|
|||||||
public readonly level?: RecommendationLevel,
|
public readonly level?: RecommendationLevel,
|
||||||
) {
|
) {
|
||||||
super(name);
|
super(name);
|
||||||
if (!code) {
|
|
||||||
throw new Error('missing code');
|
|
||||||
}
|
|
||||||
validateLevel(level);
|
validateLevel(level);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,14 +3,14 @@ import { IScriptCode } from './IScriptCode';
|
|||||||
export class ScriptCode implements IScriptCode {
|
export class ScriptCode implements IScriptCode {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly execute: string,
|
public readonly execute: string,
|
||||||
public readonly revert: string,
|
public readonly revert: string | undefined,
|
||||||
) {
|
) {
|
||||||
validateCode(execute);
|
validateCode(execute);
|
||||||
validateRevertCode(revert, execute);
|
validateRevertCode(revert, execute);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function validateRevertCode(revertCode: string, execute: string) {
|
function validateRevertCode(revertCode: string | undefined, execute: string) {
|
||||||
if (!revertCode) {
|
if (!revertCode) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,7 @@ function validateRevertCode(revertCode: string, execute: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function validateCode(code: string): void {
|
function validateCode(code: string): void {
|
||||||
if (!code || code.length === 0) {
|
if (code.length === 0) {
|
||||||
throw new Error('missing code');
|
throw new Error('missing code');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,14 +6,13 @@ export class CodeRunner {
|
|||||||
constructor(
|
constructor(
|
||||||
private readonly system = getWindowInjectedSystemOperations(),
|
private readonly system = getWindowInjectedSystemOperations(),
|
||||||
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
private readonly environment = RuntimeEnvironment.CurrentEnvironment,
|
||||||
) {
|
) { }
|
||||||
if (!system) {
|
|
||||||
throw new Error('missing system operations');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
public async runCode(code: string, folderName: string, fileExtension: string): Promise<void> {
|
||||||
const { os } = this.environment;
|
const { os } = this.environment;
|
||||||
|
if (os === undefined) {
|
||||||
|
throw new Error('Unidentified operating system');
|
||||||
|
}
|
||||||
const dir = this.system.location.combinePaths(
|
const dir = this.system.location.combinePaths(
|
||||||
this.system.operatingSystem.getTempDirectory(),
|
this.system.operatingSystem.getTempDirectory(),
|
||||||
folderName,
|
folderName,
|
||||||
|
|||||||
@@ -2,9 +2,6 @@ import { IEnvironmentVariables } from './IEnvironmentVariables';
|
|||||||
|
|
||||||
/* Validation is externalized to keep the environment objects simple */
|
/* Validation is externalized to keep the environment objects simple */
|
||||||
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
export function validateEnvironmentVariables(environment: IEnvironmentVariables): void {
|
||||||
if (!environment) {
|
|
||||||
throw new Error('missing environment');
|
|
||||||
}
|
|
||||||
const keyValues = capturePropertyValues(environment);
|
const keyValues = capturePropertyValues(environment);
|
||||||
if (!Object.keys(keyValues).length) {
|
if (!Object.keys(keyValues).length) {
|
||||||
throw new Error('Unable to capture key/value pairs');
|
throw new Error('Unable to capture key/value pairs');
|
||||||
@@ -30,7 +27,7 @@ function getKeysMissingValues(keyValuePairs: Record<string, unknown>): string[]
|
|||||||
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
* Necessary because code transformations can make class getters non-enumerable during bundling.
|
||||||
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
* This ensures that even if getters are non-enumerable, their values are still captured and used.
|
||||||
*/
|
*/
|
||||||
function capturePropertyValues(instance: unknown): Record<string, unknown> {
|
function capturePropertyValues(instance: object): Record<string, unknown> {
|
||||||
const obj: Record<string, unknown> = {};
|
const obj: Record<string, unknown> = {};
|
||||||
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
const descriptors = Object.getOwnPropertyDescriptors(instance.constructor.prototype);
|
||||||
|
|
||||||
|
|||||||
@@ -9,12 +9,9 @@ export class EventSubscriptionCollection implements IEventSubscriptionCollection
|
|||||||
}
|
}
|
||||||
|
|
||||||
public register(subscriptions: IEventSubscription[]) {
|
public register(subscriptions: IEventSubscription[]) {
|
||||||
if (!subscriptions || subscriptions.length === 0) {
|
if (subscriptions.length === 0) {
|
||||||
throw new Error('missing subscriptions');
|
throw new Error('missing subscriptions');
|
||||||
}
|
}
|
||||||
if (subscriptions.some((subscription) => !subscription)) {
|
|
||||||
throw new Error('missing subscription in list');
|
|
||||||
}
|
|
||||||
this.subscriptions.push(...subscriptions);
|
this.subscriptions.push(...subscriptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,5 +5,5 @@ export interface IEventSubscriptionCollection {
|
|||||||
|
|
||||||
register(subscriptions: IEventSubscription[]): void;
|
register(subscriptions: IEventSubscription[]): void;
|
||||||
unsubscribeAll(): void;
|
unsubscribeAll(): void;
|
||||||
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]);
|
unsubscribeAllAndRegister(subscriptions: IEventSubscription[]): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
import { ILogger } from './ILogger';
|
import { ILogger } from './ILogger';
|
||||||
|
|
||||||
export class ConsoleLogger implements ILogger {
|
export class ConsoleLogger implements ILogger {
|
||||||
constructor(private readonly globalConsole: Partial<Console> = console) {
|
constructor(private readonly consoleProxy: Partial<Console> = console) {
|
||||||
if (!globalConsole) {
|
if (!consoleProxy) { // do not trust strictNullChecks for global objects
|
||||||
throw new Error('missing console');
|
throw new Error('missing console');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public info(...params: unknown[]): void {
|
public info(...params: unknown[]): void {
|
||||||
this.globalConsole.info(...params);
|
const logFunction = this.consoleProxy?.info;
|
||||||
|
if (!logFunction) {
|
||||||
|
throw new Error('missing "info" function');
|
||||||
|
}
|
||||||
|
logFunction.call(this.consoleProxy, ...params);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,11 @@ export function createElectronLogger(logger: Partial<ElectronLog>): ILogger {
|
|||||||
throw new Error('missing logger');
|
throw new Error('missing logger');
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
info: (...params) => logger.info(...params),
|
info: (...params) => {
|
||||||
|
if (!logger.info) {
|
||||||
|
throw new Error('missing "info" function');
|
||||||
|
}
|
||||||
|
logger.info(...params);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user