Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
704a3d0417 |
25
.github/workflows/checks.quality.yaml
vendored
25
.github/workflows/checks.quality.yaml
vendored
@@ -74,28 +74,3 @@ jobs:
|
|||||||
-
|
-
|
||||||
name: Analyzing the code with pylint
|
name: Analyzing the code with pylint
|
||||||
run: npm run lint:pylint
|
run: npm run lint:pylint
|
||||||
|
|
||||||
validate-collection-files:
|
|
||||||
runs-on: ${{ matrix.os }}-latest
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
os: [ macos, ubuntu, windows ]
|
|
||||||
fail-fast: false # Still interested to see results from other combinations
|
|
||||||
steps:
|
|
||||||
-
|
|
||||||
name: Checkout
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
-
|
|
||||||
name: Setup node
|
|
||||||
uses: ./.github/actions/setup-node
|
|
||||||
-
|
|
||||||
name: Set up Python
|
|
||||||
uses: actions/setup-python@v5
|
|
||||||
with:
|
|
||||||
python-version: '3.x'
|
|
||||||
-
|
|
||||||
name: Install dependencies
|
|
||||||
run: python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
|
||||||
-
|
|
||||||
name: Validate
|
|
||||||
run: python3 ./scripts/validate-collections-yaml
|
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -14,7 +14,3 @@ node_modules
|
|||||||
|
|
||||||
# macOS
|
# macOS
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
# Python
|
|
||||||
__pycache__
|
|
||||||
.venv
|
|
||||||
4
.vscode/extensions.json
vendored
4
.vscode/extensions.json
vendored
@@ -5,10 +5,8 @@
|
|||||||
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
"wengerk.highlight-bad-chars", // Highlights bad chars.
|
||||||
"wayou.vscode-todo-highlight", // Highlights TODO.
|
"wayou.vscode-todo-highlight", // Highlights TODO.
|
||||||
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
"wix.vscode-import-cost", // Shows in KB how much a require include in code.
|
||||||
// Markdown
|
// Documentation
|
||||||
"davidanson.vscode-markdownlint", // Lints markdown.
|
"davidanson.vscode-markdownlint", // Lints markdown.
|
||||||
// YAML
|
|
||||||
"redhat.vscode-yaml", // Lints YAML files, validates against schema.
|
|
||||||
// TypeScript / JavaScript
|
// TypeScript / JavaScript
|
||||||
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
"dbaeumer.vscode-eslint", // Lints JavaScript/TypeScript.
|
||||||
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
"pmneo.tsimporter", // Provides better auto-complete for TypeScripts imports.
|
||||||
|
|||||||
27
CHANGELOG.md
27
CHANGELOG.md
@@ -1,32 +1,5 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## 0.13.5 (2024-06-26)
|
|
||||||
|
|
||||||
* ci/cd: centralize and bump artifact uploads | [22d6c79](https://github.com/undergroundwires/privacy.sexy/commit/22d6c7991eb2c138578a7d41950f301906dbf703)
|
|
||||||
* win: document and improve Firefox telemetry #259 | [8341411](https://github.com/undergroundwires/privacy.sexy/commit/8341411be434c6d145e942b1792020ccf02f58c8)
|
|
||||||
* Add image to `README.md` to thank supporters | [fa2a92b](https://github.com/undergroundwires/privacy.sexy/commit/fa2a92bf893933bf5cd04512a712b7aa1b921277)
|
|
||||||
* win: improve executable blocking, Chrome reporting | [f21ef92](https://github.com/undergroundwires/privacy.sexy/commit/f21ef9250a2f459dbd4f789d857c78298fc202e6)
|
|
||||||
* mac: discourage and document captive portal script | [b29cd7b](https://github.com/undergroundwires/privacy.sexy/commit/b29cd7b5f74accf92c9700c3171670f82c8cb3b3)
|
|
||||||
* win: fix revert scripts for removing shortcuts | [8becc7d](https://github.com/undergroundwires/privacy.sexy/commit/8becc7dbc46af4441900e9841a716a53735bc82e)
|
|
||||||
* Refactor to unify scripts/categories as Executable | [c138f74](https://github.com/undergroundwires/privacy.sexy/commit/c138f74460bafaba3da55a65f3942bb6f95b1d99)
|
|
||||||
* Add object property validation in parser #369 | [6ecfa9b](https://github.com/undergroundwires/privacy.sexy/commit/6ecfa9b954edc10401acaf5c735eec0fc9f991cd)
|
|
||||||
* win: fix missing app access recommendations #369 | [1c2d82d](https://github.com/undergroundwires/privacy.sexy/commit/1c2d82dc9bd412ea601ab2550ba0b4f7d144f8e8)
|
|
||||||
* win: fix text and handwriting script omission #369 | [1a10cf2](https://github.com/undergroundwires/privacy.sexy/commit/1a10cf2e5f87cd8eb421ef77f6ce764b5482515e)
|
|
||||||
* mac: document, improve, encourage clearing logs | [e9a5285](https://github.com/undergroundwires/privacy.sexy/commit/e9a52859f63609c3f56def0b3e4d1ac6e5661536)
|
|
||||||
* Add schema validation for collection files #369 | [dc03bff](https://github.com/undergroundwires/privacy.sexy/commit/dc03bff324d673101002bb16f14e0429e8170fbb)
|
|
||||||
* win: fix incomplete VSCEIP, location scripts | [48761f6](https://github.com/undergroundwires/privacy.sexy/commit/48761f62a242f0910307994271cbe6730fb30f7e)
|
|
||||||
* Add type validation for parameters and fix types | [fac26a6](https://github.com/undergroundwires/privacy.sexy/commit/fac26a6ca07479c84fe62c5ea2a572dad1898ef8)
|
|
||||||
* Bump Electron to latest | [ed93614](https://github.com/undergroundwires/privacy.sexy/commit/ed93614ca34b1ab166e645cc5bedd497b0caeaac)
|
|
||||||
* Trim compiler error output for better readability | [78c62cf](https://github.com/undergroundwires/privacy.sexy/commit/78c62cfc953dbba543d8bdc42828a4ef4b13a7c7)
|
|
||||||
* win: fix errors due to missing Edge uninstaller | [2f82873](https://github.com/undergroundwires/privacy.sexy/commit/2f828735a87f98ba87b4fc826823d1482d4f2db2)
|
|
||||||
* win: fix latest Edge removal on Windows 10 #309 | [e7031a3](https://github.com/undergroundwires/privacy.sexy/commit/e7031a3ae4e57b6522c6ca67fc30e8a8718506b2)
|
|
||||||
* win: categorize, rename, doc Chrome & Edge scripts | [f286f92](https://github.com/undergroundwires/privacy.sexy/commit/f286f92b1fec49e89eea8982dffbc3d6ef1defde)
|
|
||||||
* win: add disabling Edge/WebView2 auto-updates #309 | [ed7e69c](https://github.com/undergroundwires/privacy.sexy/commit/ed7e69c07efe83fdb7f4af13aa220ff991fbbe59)
|
|
||||||
* win, linux, mac: fix typos #373 | [c09c5ff](https://github.com/undergroundwires/privacy.sexy/commit/c09c5ffa47865f7c76910644558b6783ed44f1e4)
|
|
||||||
* win: add more Edge scripts including AI & ads | [1430d52](https://github.com/undergroundwires/privacy.sexy/commit/1430d5215ab094d8201710761d631dc2bd740918)
|
|
||||||
|
|
||||||
[compare](https://github.com/undergroundwires/privacy.sexy/compare/0.13.4...0.13.5)
|
|
||||||
|
|
||||||
## 0.13.4 (2024-05-27)
|
## 0.13.4 (2024-05-27)
|
||||||
|
|
||||||
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
* Add specific empty function name compiler error | [870120b](https://github.com/undergroundwires/privacy.sexy/commit/870120bc13909a3681e0f0a2351806849476342f)
|
||||||
|
|||||||
@@ -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.13.5/privacy.sexy-Setup-0.13.5.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.5/privacy.sexy-0.13.5.AppImage). For more options, see [here](#additional-install-options).
|
- 🖥️ **Offline**: Download directly for: [Windows](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-Setup-0.13.4.exe), [macOS](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.dmg), [Linux](https://github.com/undergroundwires/privacy.sexy/releases/download/0.13.4/privacy.sexy-0.13.4.AppImage). For more options, see [here](#additional-install-options).
|
||||||
|
|
||||||
See also:
|
See also:
|
||||||
|
|
||||||
@@ -186,7 +186,3 @@ Check [architecture.md](./docs/architecture.md) for an overview of design and ho
|
|||||||
Security is a top priority at privacy.sexy.
|
Security is a top priority at privacy.sexy.
|
||||||
An extensive commitment to security verification ensures this priority.
|
An extensive commitment to security verification ensures this priority.
|
||||||
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
For any security concerns or vulnerabilities, please consult the [Security Policy](./SECURITY.md).
|
||||||
|
|
||||||
## Supporters
|
|
||||||
|
|
||||||
[](https://undergroundwires.dev/supporters)
|
|
||||||
|
|||||||
@@ -41,5 +41,5 @@ Application layer compiles templating syntax during parsing to create the end sc
|
|||||||
|
|
||||||
The steps to extend the templating syntax:
|
The steps to extend the templating syntax:
|
||||||
|
|
||||||
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Executable/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
1. Add a new parser under [SyntaxParsers](./../src/application/Parser/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more.
|
||||||
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Collection files
|
# Collection files
|
||||||
|
|
||||||
privacy.sexy is a data-driven application that reads YAML files.
|
privacy.sexy is a data-driven application that reads YAML files.
|
||||||
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model. The YAML schema [`.schema.yaml`](./../src/application/collections/.schema.yaml) is provided to provide better IDE support and be used in automated validations.
|
This document details the structure and syntax of the YAML files located in [`application/collections`](./../src/application/collections/), which form the backbone of the application's data model.
|
||||||
|
|
||||||
Related documentation:
|
Related documentation:
|
||||||
|
|
||||||
- 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
|
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types.
|
||||||
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
- 📖 [Script Guidelines](./script-guidelines.md) provide guidance on script creation including best-practices.
|
||||||
|
|
||||||
## Objects
|
## Objects
|
||||||
@@ -28,22 +28,11 @@ Related documentation:
|
|||||||
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
- `scripting:` ***[`ScriptingDefinition`](#scriptingdefinition)*** **(required)**
|
||||||
- Sets the scripting language for all inline code used within the collection.
|
- Sets the scripting language for all inline code used within the collection.
|
||||||
|
|
||||||
### Executables
|
### `Category`
|
||||||
|
|
||||||
They represent independently executable actions with documentation and reversibility.
|
|
||||||
|
|
||||||
An Executable is a logical entity that can
|
|
||||||
|
|
||||||
- execute once compiled,
|
|
||||||
- include a `docs` property for documentation.
|
|
||||||
|
|
||||||
It's either [Category](#category) or a [Script](#script).
|
|
||||||
|
|
||||||
#### `Category`
|
|
||||||
|
|
||||||
Represents a logical group of scripts and subcategories.
|
Represents a logical group of scripts and subcategories.
|
||||||
|
|
||||||
##### `Category` syntax
|
#### `Category` syntax
|
||||||
|
|
||||||
- `category:` *`string`* **(required)**
|
- `category:` *`string`* **(required)**
|
||||||
- Name of the category.
|
- Name of the category.
|
||||||
@@ -54,7 +43,7 @@ Represents a logical group of scripts and subcategories.
|
|||||||
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
- `docs`: *`string`* | `[`*`string`*`, ... ]`
|
||||||
- Markdown-formatted documentation related to the category.
|
- Markdown-formatted documentation related to the category.
|
||||||
|
|
||||||
#### `Script`
|
### `Script`
|
||||||
|
|
||||||
Represents an individual tweak.
|
Represents an individual tweak.
|
||||||
|
|
||||||
@@ -69,7 +58,7 @@ Types (like [functions](#function)):
|
|||||||
|
|
||||||
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
📖 For detailed guidelines, see [Script Guidelines](./script-guidelines.md).
|
||||||
|
|
||||||
##### `Script` syntax
|
#### `Script` syntax
|
||||||
|
|
||||||
- `name`: *`string`* **(required)**
|
- `name`: *`string`* **(required)**
|
||||||
- Script name.
|
- Script name.
|
||||||
|
|||||||
@@ -80,10 +80,8 @@ See [ci-cd.md](./ci-cd.md) for more information.
|
|||||||
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
- [**`npm run install-deps [-- <options>]`**](../scripts/npm-install.js):
|
||||||
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
- Manages NPM dependency installation, it offers capabilities like doing a fresh install, retries on network errors, and other features.
|
||||||
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
- For example, you can run `npm run install-deps -- --fresh` to do clean installation of dependencies.
|
||||||
- [**`python3 ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py):
|
||||||
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
- Optimizes Visual Studio Code settings and installs essential extensions, enhancing the development environment.
|
||||||
- [**`python3 ./scripts/validate-collections-yaml`**](../scripts/validate-collections-yaml/README.md):
|
|
||||||
- Validates the syntax and structure of collection YAML files.
|
|
||||||
|
|
||||||
#### Automation scripts
|
#### Automation scripts
|
||||||
|
|
||||||
|
|||||||
5416
package-lock.json
generated
5416
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
56
package.json
56
package.json
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "privacy.sexy",
|
"name": "privacy.sexy",
|
||||||
"version": "0.13.5",
|
"version": "0.13.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"slogan": "Privacy is sexy",
|
"slogan": "Privacy is sexy",
|
||||||
"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.",
|
||||||
@@ -34,54 +34,54 @@
|
|||||||
"postuninstall": "electron-builder install-app-deps"
|
"postuninstall": "electron-builder install-app-deps"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@floating-ui/vue": "^1.1.1",
|
"@floating-ui/vue": "^1.0.6",
|
||||||
"@juggle/resize-observer": "^3.4.0",
|
"@juggle/resize-observer": "^3.4.0",
|
||||||
"ace-builds": "^1.35.3",
|
"ace-builds": "^1.33.0",
|
||||||
"electron-log": "^5.1.6",
|
"electron-log": "^5.1.2",
|
||||||
"electron-progressbar": "^2.2.1",
|
"electron-progressbar": "^2.2.1",
|
||||||
"electron-updater": "^6.2.1",
|
"electron-updater": "^6.1.9",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
"markdown-it": "^14.1.0",
|
"markdown-it": "^14.1.0",
|
||||||
"vue": "^3.4.32"
|
"vue": "^3.4.27"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
"@modyfi/vite-plugin-yaml": "^1.1.0",
|
||||||
"@rushstack/eslint-patch": "^1.10.3",
|
"@rushstack/eslint-patch": "^1.10.2",
|
||||||
"@types/ace": "^0.0.52",
|
"@types/ace": "^0.0.52",
|
||||||
"@types/file-saver": "^2.0.7",
|
"@types/file-saver": "^2.0.7",
|
||||||
"@types/markdown-it": "^14.1.1",
|
"@types/markdown-it": "^14.0.1",
|
||||||
"@typescript-eslint/eslint-plugin": "6.21.0",
|
"@typescript-eslint/eslint-plugin": "6.21.0",
|
||||||
"@typescript-eslint/parser": "6.21.0",
|
"@typescript-eslint/parser": "6.21.0",
|
||||||
"@vitejs/plugin-legacy": "^5.4.1",
|
"@vitejs/plugin-legacy": "^5.3.2",
|
||||||
"@vitejs/plugin-vue": "^5.0.5",
|
"@vitejs/plugin-vue": "^5.0.4",
|
||||||
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
"@vue/eslint-config-airbnb-with-typescript": "^8.0.0",
|
||||||
"@vue/eslint-config-typescript": "12.0.0",
|
"@vue/eslint-config-typescript": "12.0.0",
|
||||||
"@vue/test-utils": "^2.4.6",
|
"@vue/test-utils": "^2.4.5",
|
||||||
"autoprefixer": "^10.4.19",
|
"autoprefixer": "^10.4.19",
|
||||||
"cypress": "^13.13.1",
|
"cypress": "^13.7.3",
|
||||||
"electron": "^31.2.1",
|
"electron": "^29.3.0",
|
||||||
"electron-builder": "^24.13.3",
|
"electron-builder": "^24.13.3",
|
||||||
"electron-devtools-installer": "^3.2.0",
|
"electron-devtools-installer": "^3.2.0",
|
||||||
"electron-vite": "^2.3.0",
|
"electron-vite": "^2.1.0",
|
||||||
"eslint": "8.57.0",
|
"eslint": "8.57.0",
|
||||||
"eslint-plugin-cypress": "^3.3.0",
|
"eslint-plugin-cypress": "^2.15.1",
|
||||||
"eslint-plugin-vue": "^9.27.0",
|
"eslint-plugin-vue": "^9.25.0",
|
||||||
"eslint-plugin-vuejs-accessibility": "^2.4.0",
|
"eslint-plugin-vuejs-accessibility": "^2.2.1",
|
||||||
"jsdom": "^24.1.0",
|
"jsdom": "^24.0.0",
|
||||||
"markdownlint-cli": "^0.41.0",
|
"markdownlint-cli": "^0.39.0",
|
||||||
"postcss": "^8.4.39",
|
"postcss": "^8.4.38",
|
||||||
"remark-cli": "^12.0.1",
|
"remark-cli": "^12.0.0",
|
||||||
"remark-lint-no-dead-urls": "^1.1.0",
|
"remark-lint-no-dead-urls": "^1.1.0",
|
||||||
"remark-preset-lint-consistent": "^6.0.0",
|
"remark-preset-lint-consistent": "^6.0.0",
|
||||||
"remark-validate-links": "^13.0.1",
|
"remark-validate-links": "^13.0.1",
|
||||||
"sass": "^1.77.8",
|
"sass": "^1.75.0",
|
||||||
"start-server-and-test": "^2.0.4",
|
"start-server-and-test": "^2.0.3",
|
||||||
"terser": "^5.31.3",
|
"terser": "^5.30.3",
|
||||||
"tslib": "^2.6.3",
|
"tslib": "^2.6.2",
|
||||||
"typescript": "^5.4.5",
|
"typescript": "^5.4.5",
|
||||||
"vite": "^5.3.4",
|
"vite": "^5.2.8",
|
||||||
"vitest": "^2.0.3",
|
"vitest": "^1.5.0",
|
||||||
"vue-tsc": "^2.0.26",
|
"vue-tsc": "^2.0.13",
|
||||||
"yaml-lint": "^1.7.0"
|
"yaml-lint": "^1.7.0"
|
||||||
},
|
},
|
||||||
"//devDependencies": {
|
"//devDependencies": {
|
||||||
|
|||||||
@@ -58,10 +58,6 @@ def add_or_update_settings() -> None:
|
|||||||
# Details: # pylint: disable-next=line-too-long
|
# Details: # pylint: disable-next=line-too-long
|
||||||
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
# - https://archive.ph/2024.01.06-003914/https://github.com/microsoft/vscode/issues/179274, https://web.archive.org/web/20240106003915/https://github.com/microsoft/vscode/issues/179274
|
||||||
|
|
||||||
# Disable telemetry
|
|
||||||
configure_setting_key('redhat.telemetry.enabled', False)
|
|
||||||
configure_setting_key('gitlens.telemetry.enabled', False)
|
|
||||||
|
|
||||||
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
def configure_setting_key(configuration_key: str, desired_value: Any) -> None:
|
||||||
try:
|
try:
|
||||||
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
with open(VSCODE_SETTINGS_JSON_FILE, 'r+', encoding='utf-8') as file:
|
||||||
|
|||||||
@@ -1,51 +0,0 @@
|
|||||||
# validate-collections-yaml
|
|
||||||
|
|
||||||
This script validates YAML collection files against a predefined schema to ensure their integrity.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Python 3.x installed on your system.
|
|
||||||
|
|
||||||
## Running in a Virtual Environment (Recommended)
|
|
||||||
|
|
||||||
Using a virtual environment isolates dependencies and prevents conflicts.
|
|
||||||
|
|
||||||
1. **Create a virtual environment:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m venv ./scripts/validate-collections-yaml/.venv
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Activate the virtual environment:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
source ./scripts/validate-collections-yaml/.venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Install dependencies:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Run the script:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ./scripts/validate-collections-yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Globally
|
|
||||||
|
|
||||||
Running the script globally is less recommended due to potential dependency conflicts.
|
|
||||||
|
|
||||||
1. **Install dependencies:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 -m pip install -r ./scripts/validate-collections-yaml/requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Run the script:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
python3 ./scripts/validate-collections-yaml
|
|
||||||
```
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
"""
|
|
||||||
Description:
|
|
||||||
This script validates collection YAML files against the expected schema.
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
python3 ./scripts/validate-collections-yaml
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
This script requires the `jsonschema` and `pyyaml` packages (see requirements.txt).
|
|
||||||
"""
|
|
||||||
# pylint: disable=missing-function-docstring
|
|
||||||
from os import path
|
|
||||||
import sys
|
|
||||||
from glob import glob
|
|
||||||
from typing import List
|
|
||||||
from jsonschema import exceptions, validate # pylint: disable=import-error
|
|
||||||
import yaml # pylint: disable=import-error
|
|
||||||
|
|
||||||
SCHEMA_FILE_PATH = './src/application/collections/.schema.yaml'
|
|
||||||
COLLECTIONS_GLOB_PATTERN = './src/application/collections/*.yaml'
|
|
||||||
|
|
||||||
def main() -> None:
|
|
||||||
schema_yaml = read_file(SCHEMA_FILE_PATH)
|
|
||||||
schema_json = convert_yaml_to_json(schema_yaml)
|
|
||||||
collection_file_paths = find_collection_files(COLLECTIONS_GLOB_PATTERN)
|
|
||||||
print(f'Found {len(collection_file_paths)} YAML files to validate.')
|
|
||||||
|
|
||||||
total_invalid_files = 0
|
|
||||||
for collection_file_path in collection_file_paths:
|
|
||||||
file_name = path.basename(collection_file_path)
|
|
||||||
print(f'Validating {file_name}...')
|
|
||||||
collection_yaml = read_file(collection_file_path)
|
|
||||||
collection_json = convert_yaml_to_json(collection_yaml)
|
|
||||||
try:
|
|
||||||
validate(instance=collection_json, schema=schema_json)
|
|
||||||
print(f'Success: {file_name} is valid.')
|
|
||||||
except exceptions.ValidationError as err:
|
|
||||||
print(f'Error: Validation failed for {file_name}.', file=sys.stderr)
|
|
||||||
print(str(err), file=sys.stderr)
|
|
||||||
total_invalid_files += 1
|
|
||||||
|
|
||||||
if total_invalid_files > 0:
|
|
||||||
print(f'Validation complete with {total_invalid_files} invalid files.', file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
else:
|
|
||||||
print('Validation complete. All files are valid.')
|
|
||||||
sys.exit(0)
|
|
||||||
|
|
||||||
def read_file(file_path: str) -> str:
|
|
||||||
with open(file_path, 'r', encoding='utf-8') as file:
|
|
||||||
return file.read()
|
|
||||||
|
|
||||||
def find_collection_files(glob_pattern: str) -> List[str]:
|
|
||||||
files = glob(glob_pattern)
|
|
||||||
filtered_files = [f for f in files if not path.basename(f).startswith('.')]
|
|
||||||
return filtered_files
|
|
||||||
|
|
||||||
def convert_yaml_to_json(yaml_content: str) -> dict:
|
|
||||||
return yaml.safe_load(yaml_content)
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
attrs==23.2.0
|
|
||||||
jsonschema==4.22.0
|
|
||||||
jsonschema-specifications==2023.12.1
|
|
||||||
PyYAML==6.0.1
|
|
||||||
referencing==0.35.1
|
|
||||||
rpds-py==0.18.1
|
|
||||||
@@ -91,7 +91,7 @@ async function verifyFilesExist(directoryPath, filePatterns) {
|
|||||||
if (!match) {
|
if (!match) {
|
||||||
die(
|
die(
|
||||||
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
`No file matches the pattern ${pattern.source} in directory \`${directoryPath}\``,
|
||||||
`\nFiles in directory:\n${files.map((file) => `- ${file}`).join('\n')}`,
|
`\nFiles in directory:\n${files.map((file) => `\t- ${file}`).join('\n')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,13 +5,13 @@ export type EnumType = number | string;
|
|||||||
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
export type EnumVariable<T extends EnumType, TEnumValue extends EnumType>
|
||||||
= { [key in T]: TEnumValue };
|
= { [key in T]: TEnumValue };
|
||||||
|
|
||||||
export interface EnumParser<TEnum> {
|
export interface IEnumParser<TEnum> {
|
||||||
parseEnum(value: string, propertyName: string): TEnum;
|
parseEnum(value: string, propertyName: string): TEnum;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||||
enumVariable: EnumVariable<T, TEnumValue>,
|
enumVariable: EnumVariable<T, TEnumValue>,
|
||||||
): EnumParser<TEnumValue> {
|
): IEnumParser<TEnumValue> {
|
||||||
return {
|
return {
|
||||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import { isArray } from '@/TypeHelpers';
|
|
||||||
|
|
||||||
export type OptionalString = string | undefined | null;
|
|
||||||
|
|
||||||
export function filterEmptyStrings(
|
|
||||||
texts: readonly OptionalString[],
|
|
||||||
isArrayType: typeof isArray = isArray,
|
|
||||||
): string[] {
|
|
||||||
if (!isArrayType(texts)) {
|
|
||||||
throw new Error(`Invalid input: Expected an array, but received type ${typeof texts}.`);
|
|
||||||
}
|
|
||||||
assertArrayItemsAreStringLike(texts);
|
|
||||||
return texts
|
|
||||||
.filter((title): title is string => Boolean(title));
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertArrayItemsAreStringLike(
|
|
||||||
texts: readonly unknown[],
|
|
||||||
): asserts texts is readonly OptionalString[] {
|
|
||||||
const invalidItems = texts.filter((item) => !(typeof item === 'string' || item === undefined || item === null));
|
|
||||||
if (invalidItems.length > 0) {
|
|
||||||
const invalidTypes = invalidItems.map((item) => typeof item).join(', ');
|
|
||||||
throw new Error(`Invalid array items: Expected items as string, undefined, or null. Received invalid types: ${invalidTypes}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import { splitTextIntoLines } from './SplitTextIntoLines';
|
|
||||||
|
|
||||||
export function indentText(
|
|
||||||
text: string,
|
|
||||||
indentLevel = 1,
|
|
||||||
utilities: TextIndentationUtilities = DefaultUtilities,
|
|
||||||
): string {
|
|
||||||
if (!utilities.isStringType(text)) {
|
|
||||||
throw new Error(`Indentation error: The input must be a string. Received type: ${typeof text}.`);
|
|
||||||
}
|
|
||||||
if (indentLevel <= 0) {
|
|
||||||
throw new Error(`Indentation error: The indent level must be a positive integer. Received: ${indentLevel}.`);
|
|
||||||
}
|
|
||||||
const indentation = '\t'.repeat(indentLevel);
|
|
||||||
return utilities.splitIntoLines(text)
|
|
||||||
.map((line) => (line ? `${indentation}${line}` : line))
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
interface TextIndentationUtilities {
|
|
||||||
readonly splitIntoLines: typeof splitTextIntoLines;
|
|
||||||
readonly isStringType: typeof isString;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: TextIndentationUtilities = {
|
|
||||||
splitIntoLines: splitTextIntoLines,
|
|
||||||
isStringType: isString,
|
|
||||||
};
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
|
|
||||||
export function splitTextIntoLines(
|
|
||||||
text: string,
|
|
||||||
isStringType = isString,
|
|
||||||
): string[] {
|
|
||||||
if (!isStringType(text)) {
|
|
||||||
throw new Error(`Line splitting error: Expected a string but received type '${typeof text}'.`);
|
|
||||||
}
|
|
||||||
return text.split(/\r\n|\r|\n/);
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import { assertInRange } from '@/application/Common/Enum';
|
import { assertInRange } from '@/application/Common/Enum';
|
||||||
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
import { CategoryCollectionState } from './State/CategoryCollectionState';
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
|
||||||
import { ApplicationCode } from './Code/ApplicationCode';
|
import { ApplicationCode } from './Code/ApplicationCode';
|
||||||
|
|||||||
@@ -1,20 +1,18 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
import type { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
|
||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
import type { ICodeChangedEvent } from './ICodeChangedEvent';
|
||||||
|
|
||||||
export class CodeChangedEvent implements ICodeChangedEvent {
|
export class CodeChangedEvent implements ICodeChangedEvent {
|
||||||
public readonly code: string;
|
public readonly code: string;
|
||||||
|
|
||||||
public readonly addedScripts: ReadonlyArray<Script>;
|
public readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly removedScripts: ReadonlyArray<Script>;
|
public readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
public readonly changedScripts: ReadonlyArray<Script>;
|
public readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
|
|
||||||
private readonly scripts: Map<Script, ICodePosition>;
|
private readonly scripts: Map<IScript, ICodePosition>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
code: string,
|
code: string,
|
||||||
@@ -27,7 +25,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
|
||||||
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
|
||||||
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
this.changedScripts = getChangedScripts(oldScripts, newScripts);
|
||||||
this.scripts = new Map<Script, ICodePosition>();
|
this.scripts = new Map<IScript, ICodePosition>();
|
||||||
scripts.forEach((position, selection) => {
|
scripts.forEach((position, selection) => {
|
||||||
this.scripts.set(selection.script, position);
|
this.scripts.set(selection.script, position);
|
||||||
});
|
});
|
||||||
@@ -37,13 +35,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
return this.scripts.size === 0;
|
return this.scripts.size === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public getScriptPositionInCode(script: Script): ICodePosition {
|
public getScriptPositionInCode(script: IScript): ICodePosition {
|
||||||
return this.getPositionById(script.executableId);
|
return this.getPositionById(script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private getPositionById(scriptId: ExecutableId): ICodePosition {
|
private getPositionById(scriptId: string): ICodePosition {
|
||||||
const position = [...this.scripts.entries()]
|
const position = [...this.scripts.entries()]
|
||||||
.filter(([s]) => s.executableId === scriptId)
|
.filter(([s]) => s.id === scriptId)
|
||||||
.map(([, pos]) => pos)
|
.map(([, pos]) => pos)
|
||||||
.at(0);
|
.at(0);
|
||||||
if (!position) {
|
if (!position) {
|
||||||
@@ -54,12 +52,12 @@ export class CodeChangedEvent implements ICodeChangedEvent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodePosition>) {
|
||||||
const totalLines = splitTextIntoLines(script).length;
|
const totalLines = script.split(/\r\n|\r|\n/).length;
|
||||||
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
const missingPositions = positions.filter((position) => position.endLine > totalLines);
|
||||||
if (missingPositions.length > 0) {
|
if (missingPositions.length > 0) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
`Out of range script end line: "${missingPositions.map((pos) => pos.endLine).join('", "')}"`
|
||||||
+ ` (total code lines: ${totalLines}).`,
|
+ `(total code lines: ${totalLines}).`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -67,7 +65,7 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
|
|||||||
function getChangedScripts(
|
function getChangedScripts(
|
||||||
oldScripts: ReadonlyArray<SelectedScript>,
|
oldScripts: ReadonlyArray<SelectedScript>,
|
||||||
newScripts: ReadonlyArray<SelectedScript>,
|
newScripts: ReadonlyArray<SelectedScript>,
|
||||||
): ReadonlyArray<Script> {
|
): ReadonlyArray<IScript> {
|
||||||
return newScripts
|
return newScripts
|
||||||
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
|
||||||
&& oldScript.revert !== newScript.revert))
|
&& oldScript.revert !== newScript.revert))
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
import type { ICodePosition } from '@/application/Context/State/Code/Position/ICodePosition';
|
||||||
|
|
||||||
export interface ICodeChangedEvent {
|
export interface ICodeChangedEvent {
|
||||||
readonly code: string;
|
readonly code: string;
|
||||||
readonly addedScripts: ReadonlyArray<Script>;
|
readonly addedScripts: ReadonlyArray<IScript>;
|
||||||
readonly removedScripts: ReadonlyArray<Script>;
|
readonly removedScripts: ReadonlyArray<IScript>;
|
||||||
readonly changedScripts: ReadonlyArray<Script>;
|
readonly changedScripts: ReadonlyArray<IScript>;
|
||||||
isEmpty(): boolean;
|
isEmpty(): boolean;
|
||||||
getScriptPositionInCode(script: Script): ICodePosition;
|
getScriptPositionInCode(script: IScript): ICodePosition;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
|
||||||
import type { ICodeBuilder } from './ICodeBuilder';
|
import type { ICodeBuilder } from './ICodeBuilder';
|
||||||
|
|
||||||
const TotalFunctionSeparatorChars = 58;
|
const TotalFunctionSeparatorChars = 58;
|
||||||
@@ -16,7 +15,7 @@ export abstract class CodeBuilder implements ICodeBuilder {
|
|||||||
this.lines.push('');
|
this.lines.push('');
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
const lines = splitTextIntoLines(code);
|
const lines = code.match(/[^\r\n]+/g);
|
||||||
if (lines) {
|
if (lines) {
|
||||||
this.lines.push(...lines);
|
this.lines.push(...lines);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { FilterChange } from './Event/FilterChange';
|
import { FilterChange } from './Event/FilterChange';
|
||||||
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
|
||||||
import type { FilterResult } from './Result/FilterResult';
|
import type { FilterResult } from './Result/FilterResult';
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import type { FilterResult } from './FilterResult';
|
import type { FilterResult } from './FilterResult';
|
||||||
|
|
||||||
export class AppliedFilterResult implements FilterResult {
|
export class AppliedFilterResult implements FilterResult {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly scriptMatches: ReadonlyArray<Script>,
|
public readonly scriptMatches: ReadonlyArray<IScript>,
|
||||||
public readonly categoryMatches: ReadonlyArray<Category>,
|
public readonly categoryMatches: ReadonlyArray<ICategory>,
|
||||||
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'); }
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { IScript, ICategory } from '@/domain/ICategory';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
|
|
||||||
export interface FilterResult {
|
export interface FilterResult {
|
||||||
readonly categoryMatches: ReadonlyArray<Category>;
|
readonly categoryMatches: ReadonlyArray<ICategory>;
|
||||||
readonly scriptMatches: ReadonlyArray<Script>;
|
readonly scriptMatches: ReadonlyArray<IScript>;
|
||||||
readonly query: string;
|
readonly query: string;
|
||||||
hasAnyMatches(): boolean;
|
hasAnyMatches(): boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import type { FilterResult } from '../Result/FilterResult';
|
import type { FilterResult } from '../Result/FilterResult';
|
||||||
|
|
||||||
export interface FilterStrategy {
|
export interface FilterStrategy {
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { ICategory, IScript } from '@/domain/ICategory';
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
import type { IScriptCode } from '@/domain/IScriptCode';
|
||||||
import type { Documentable } from '@/domain/Executables/Documentable';
|
import type { IDocumentable } from '@/domain/IDocumentable';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
import { AppliedFilterResult } from '../Result/AppliedFilterResult';
|
||||||
import type { FilterStrategy } from './FilterStrategy';
|
import type { FilterStrategy } from './FilterStrategy';
|
||||||
import type { FilterResult } from '../Result/FilterResult';
|
import type { FilterResult } from '../Result/FilterResult';
|
||||||
@@ -25,7 +24,7 @@ export class LinearFilterStrategy implements FilterStrategy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesCategory(
|
function matchesCategory(
|
||||||
category: Category,
|
category: ICategory,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return matchesAny(
|
return matchesAny(
|
||||||
@@ -35,7 +34,7 @@ function matchesCategory(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchesScript(
|
function matchesScript(
|
||||||
script: Script,
|
script: IScript,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return matchesAny(
|
return matchesAny(
|
||||||
@@ -59,7 +58,7 @@ function matchName(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchCode(
|
function matchCode(
|
||||||
code: ScriptCode,
|
code: IScriptCode,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
if (code.execute.toLowerCase().includes(filterLowercase)) {
|
if (code.execute.toLowerCase().includes(filterLowercase)) {
|
||||||
@@ -72,7 +71,7 @@ function matchCode(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function matchDocumentation(
|
function matchDocumentation(
|
||||||
documentable: Documentable,
|
documentable: IDocumentable,
|
||||||
filterLowercase: string,
|
filterLowercase: string,
|
||||||
): boolean {
|
): boolean {
|
||||||
return documentable.docs.some(
|
return documentable.docs.some(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { IApplicationCode } from './Code/IApplicationCode';
|
import type { IApplicationCode } from './Code/IApplicationCode';
|
||||||
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
import type { ReadonlyFilterContext, FilterContext } from './Filter/FilterContext';
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyCategorySelection {
|
export interface ReadonlyCategorySelection {
|
||||||
areAllScriptsSelected(category: Category): boolean;
|
areAllScriptsSelected(category: ICategory): boolean;
|
||||||
isAnyScriptSelected(category: Category): boolean;
|
isAnyScriptSelected(category: ICategory): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CategorySelection extends ReadonlyCategorySelection {
|
export interface CategorySelection extends ReadonlyCategorySelection {
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
|
|
||||||
type CategorySelectionStatus = {
|
type CategorySelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -8,7 +6,7 @@ type CategorySelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface CategorySelectionChange {
|
export interface CategorySelectionChange {
|
||||||
readonly categoryId: ExecutableId;
|
readonly categoryId: number;
|
||||||
readonly newStatus: CategorySelectionStatus;
|
readonly newStatus: CategorySelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
import type { CategorySelectionChange, CategorySelectionChangeCommand } from './CategorySelectionChange';
|
||||||
import type { CategorySelection } from './CategorySelection';
|
import type { CategorySelection } from './CategorySelection';
|
||||||
import type { ScriptSelection } from '../Script/ScriptSelection';
|
import type { ScriptSelection } from '../Script/ScriptSelection';
|
||||||
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public areAllScriptsSelected(category: Category): boolean {
|
public areAllScriptsSelected(category: ICategory): boolean {
|
||||||
const { selectedScripts } = this.scriptSelection;
|
const { selectedScripts } = this.scriptSelection;
|
||||||
if (selectedScripts.length === 0) {
|
if (selectedScripts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -23,11 +23,11 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return scripts.every(
|
return scripts.every(
|
||||||
(script) => selectedScripts.some((selected) => selected.id === script.executableId),
|
(script) => selectedScripts.some((selected) => selected.id === script.id),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isAnyScriptSelected(category: Category): boolean {
|
public isAnyScriptSelected(category: ICategory): boolean {
|
||||||
const { selectedScripts } = this.scriptSelection;
|
const { selectedScripts } = this.scriptSelection;
|
||||||
if (selectedScripts.length === 0) {
|
if (selectedScripts.length === 0) {
|
||||||
return false;
|
return false;
|
||||||
@@ -50,7 +50,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
|
|||||||
const scripts = category.getAllScriptsRecursively();
|
const scripts = category.getAllScriptsRecursively();
|
||||||
const scriptsChangesInCategory = scripts
|
const scriptsChangesInCategory = scripts
|
||||||
.map((script): ScriptSelectionChange => ({
|
.map((script): ScriptSelectionChange => ({
|
||||||
scriptId: script.executableId,
|
scriptId: script.id,
|
||||||
newStatus: {
|
newStatus: {
|
||||||
...change.newStatus,
|
...change.newStatus,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import { EventSource } from '@/infrastructure/Events/EventSource';
|
import { EventSource } from '@/infrastructure/Events/EventSource';
|
||||||
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
import type { ReadonlyRepository, Repository } from '@/application/Repository/Repository';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
import { batchedDebounce } from '@/application/Common/Timing/BatchedDebounce';
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import { UserSelectedScript } from './UserSelectedScript';
|
import { UserSelectedScript } from './UserSelectedScript';
|
||||||
import type { ScriptSelection } from './ScriptSelection';
|
import type { ScriptSelection } from './ScriptSelection';
|
||||||
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChange, ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
@@ -17,7 +16,7 @@ export type DebounceFunction = typeof batchedDebounce<ScriptSelectionChangeComma
|
|||||||
export class DebouncedScriptSelection implements ScriptSelection {
|
export class DebouncedScriptSelection implements ScriptSelection {
|
||||||
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
public readonly changed = new EventSource<ReadonlyArray<SelectedScript>>();
|
||||||
|
|
||||||
private readonly scripts: Repository<SelectedScript>;
|
private readonly scripts: Repository<string, SelectedScript>;
|
||||||
|
|
||||||
public readonly processChanges: ScriptSelection['processChanges'];
|
public readonly processChanges: ScriptSelection['processChanges'];
|
||||||
|
|
||||||
@@ -26,7 +25,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||||
debounce: DebounceFunction = batchedDebounce,
|
debounce: DebounceFunction = batchedDebounce,
|
||||||
) {
|
) {
|
||||||
this.scripts = new InMemoryRepository<SelectedScript>();
|
this.scripts = new InMemoryRepository<string, SelectedScript>();
|
||||||
for (const script of selectedScripts) {
|
for (const script of selectedScripts) {
|
||||||
this.scripts.addItem(script);
|
this.scripts.addItem(script);
|
||||||
}
|
}
|
||||||
@@ -39,8 +38,8 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public isSelected(scriptExecutableId: ExecutableId): boolean {
|
public isSelected(scriptId: string): boolean {
|
||||||
return this.scripts.exists(scriptExecutableId);
|
return this.scripts.exists(scriptId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public get selectedScripts(): readonly SelectedScript[] {
|
public get selectedScripts(): readonly SelectedScript[] {
|
||||||
@@ -50,7 +49,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
public selectAll(): void {
|
public selectAll(): void {
|
||||||
const scriptsToSelect = this.collection
|
const scriptsToSelect = this.collection
|
||||||
.getAllScripts()
|
.getAllScripts()
|
||||||
.filter((script) => !this.scripts.exists(script.executableId))
|
.filter((script) => !this.scripts.exists(script.id))
|
||||||
.map((script) => new UserSelectedScript(script, false));
|
.map((script) => new UserSelectedScript(script, false));
|
||||||
if (scriptsToSelect.length === 0) {
|
if (scriptsToSelect.length === 0) {
|
||||||
return;
|
return;
|
||||||
@@ -81,7 +80,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public selectOnly(scripts: readonly Script[]): void {
|
public selectOnly(scripts: readonly IScript[]): void {
|
||||||
assertNonEmptyScriptSelection(scripts);
|
assertNonEmptyScriptSelection(scripts);
|
||||||
this.processChanges({
|
this.processChanges({
|
||||||
changes: [
|
changes: [
|
||||||
@@ -117,12 +116,12 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
private applyChange(change: ScriptSelectionChange): number {
|
private applyChange(change: ScriptSelectionChange): number {
|
||||||
const script = this.collection.getScript(change.scriptId);
|
const script = this.collection.getScript(change.scriptId);
|
||||||
if (change.newStatus.isSelected) {
|
if (change.newStatus.isSelected) {
|
||||||
return this.addOrUpdateScript(script.executableId, change.newStatus.isReverted);
|
return this.addOrUpdateScript(script.id, change.newStatus.isReverted);
|
||||||
}
|
}
|
||||||
return this.removeScript(script.executableId);
|
return this.removeScript(script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
private addOrUpdateScript(scriptId: ExecutableId, revert: boolean): number {
|
private addOrUpdateScript(scriptId: string, revert: boolean): number {
|
||||||
const script = this.collection.getScript(scriptId);
|
const script = this.collection.getScript(scriptId);
|
||||||
const selectedScript = new UserSelectedScript(script, revert);
|
const selectedScript = new UserSelectedScript(script, revert);
|
||||||
if (!this.scripts.exists(selectedScript.id)) {
|
if (!this.scripts.exists(selectedScript.id)) {
|
||||||
@@ -137,7 +136,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
private removeScript(scriptId: ExecutableId): number {
|
private removeScript(scriptId: string): number {
|
||||||
if (!this.scripts.exists(scriptId)) {
|
if (!this.scripts.exists(scriptId)) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -146,31 +145,31 @@ export class DebouncedScriptSelection implements ScriptSelection {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
|
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
|
||||||
if (selectedItems.length === 0) {
|
if (selectedItems.length === 0) {
|
||||||
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
throw new Error('Provided script array is empty. To deselect all scripts, please use the deselectAll() method instead.');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeSelected(
|
function getScriptIdsToBeSelected(
|
||||||
existingItems: ReadonlyRepository<SelectedScript>,
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
desiredScripts: readonly Script[],
|
desiredScripts: readonly IScript[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return desiredScripts
|
return desiredScripts
|
||||||
.filter((script) => !existingItems.exists(script.executableId))
|
.filter((script) => !existingItems.exists(script.id))
|
||||||
.map((script) => script.executableId);
|
.map((script) => script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getScriptIdsToBeDeselected(
|
function getScriptIdsToBeDeselected(
|
||||||
existingItems: ReadonlyRepository<SelectedScript>,
|
existingItems: ReadonlyRepository<string, SelectedScript>,
|
||||||
desiredScripts: readonly Script[],
|
desiredScripts: readonly IScript[],
|
||||||
): string[] {
|
): string[] {
|
||||||
return existingItems
|
return existingItems
|
||||||
.getItems()
|
.getItems()
|
||||||
.filter((existing) => !desiredScripts.some((script) => existing.id === script.executableId))
|
.filter((existing) => !desiredScripts.some((script) => existing.id === script.id))
|
||||||
.map((script) => script.id);
|
.map((script) => script.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
function equals(a: SelectedScript, b: SelectedScript): boolean {
|
||||||
return a.script.executableId === b.script.executableId && a.revert === b.revert;
|
return a.script.equals(b.script.id) && a.revert === b.revert;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
import type { IEventSource } from '@/infrastructure/Events/IEventSource';
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IScript } from '@/domain/IScript';
|
||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
import type { SelectedScript } from './SelectedScript';
|
import type { SelectedScript } from './SelectedScript';
|
||||||
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
|
||||||
|
|
||||||
export interface ReadonlyScriptSelection {
|
export interface ReadonlyScriptSelection {
|
||||||
readonly changed: IEventSource<readonly SelectedScript[]>;
|
readonly changed: IEventSource<readonly SelectedScript[]>;
|
||||||
readonly selectedScripts: readonly SelectedScript[];
|
readonly selectedScripts: readonly SelectedScript[];
|
||||||
isSelected(scriptExecutableId: ExecutableId): boolean;
|
isSelected(scriptId: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScriptSelection extends ReadonlyScriptSelection {
|
export interface ScriptSelection extends ReadonlyScriptSelection {
|
||||||
selectOnly(scripts: readonly Script[]): void;
|
selectOnly(scripts: readonly IScript[]): void;
|
||||||
selectAll(): void;
|
selectAll(): void;
|
||||||
deselectAll(): void;
|
deselectAll(): void;
|
||||||
processChanges(action: ScriptSelectionChangeCommand): void;
|
processChanges(action: ScriptSelectionChangeCommand): void;
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import type { ExecutableId } from '@/domain/Executables/Identifiable';
|
|
||||||
|
|
||||||
export type ScriptSelectionStatus = {
|
export type ScriptSelectionStatus = {
|
||||||
readonly isSelected: true;
|
readonly isSelected: true;
|
||||||
readonly isReverted: boolean;
|
readonly isReverted: boolean;
|
||||||
@@ -9,7 +7,7 @@ export type ScriptSelectionStatus = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export interface ScriptSelectionChange {
|
export interface ScriptSelectionChange {
|
||||||
readonly scriptId: ExecutableId;
|
readonly scriptId: string;
|
||||||
readonly newStatus: ScriptSelectionStatus;
|
readonly newStatus: ScriptSelectionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import type { IEntity } from '@/infrastructure/Entity/IEntity';
|
||||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
import type { IScript } from '@/domain/IScript';
|
||||||
|
|
||||||
export interface SelectedScript extends RepositoryEntity {
|
type ScriptId = IScript['id'];
|
||||||
readonly script: Script;
|
|
||||||
|
export interface SelectedScript extends IEntity<ScriptId> {
|
||||||
|
readonly script: IScript;
|
||||||
readonly revert: boolean;
|
readonly revert: boolean;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
|
||||||
import type { RepositoryEntity } from '@/application/Repository/RepositoryEntity';
|
import type { IScript } from '@/domain/IScript';
|
||||||
|
import type { SelectedScript } from './SelectedScript';
|
||||||
|
|
||||||
export class UserSelectedScript implements RepositoryEntity {
|
type SelectedScriptId = SelectedScript['id'];
|
||||||
public readonly id: string;
|
|
||||||
|
|
||||||
|
export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
|
||||||
constructor(
|
constructor(
|
||||||
public readonly script: Script,
|
public readonly script: IScript,
|
||||||
public readonly revert: boolean,
|
public readonly revert: boolean,
|
||||||
) {
|
) {
|
||||||
this.id = script.executableId;
|
super(script.id);
|
||||||
if (revert && !script.canRevert()) {
|
if (revert && !script.canRevert()) {
|
||||||
throw new Error(`The script with ID '${script.executableId}' is not reversible and cannot be reverted.`);
|
throw new Error(`The script with ID '${script.id}' is not reversible and cannot be reverted.`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
import { ScriptToCategorySelectionMapper } from './Category/ScriptToCategorySelectionMapper';
|
||||||
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
import { DebouncedScriptSelection } from './Script/DebouncedScriptSelection';
|
||||||
import type { CategorySelection } from './Category/CategorySelection';
|
import type { CategorySelection } from './Category/CategorySelection';
|
||||||
|
|||||||
@@ -1,48 +1,40 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import type { IApplication } from '@/domain/IApplication';
|
import type { IApplication } from '@/domain/IApplication';
|
||||||
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import WindowsData from '@/application/collections/windows.yaml';
|
import WindowsData from '@/application/collections/windows.yaml';
|
||||||
import MacOsData from '@/application/collections/macos.yaml';
|
import MacOsData from '@/application/collections/macos.yaml';
|
||||||
import LinuxData from '@/application/collections/linux.yaml';
|
import LinuxData from '@/application/collections/linux.yaml';
|
||||||
import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
|
import { parseProjectDetails } from '@/application/Parser/ProjectDetailsParser';
|
||||||
import { Application } from '@/domain/Application';
|
import { Application } from '@/domain/Application';
|
||||||
import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
|
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata';
|
||||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory';
|
||||||
|
import { parseCategoryCollection } from './CategoryCollectionParser';
|
||||||
|
|
||||||
export function parseApplication(
|
export function parseApplication(
|
||||||
collectionsData: readonly CollectionData[] = PreParsedCollections,
|
categoryParser = parseCategoryCollection,
|
||||||
utilities: ApplicationParserUtilities = DefaultUtilities,
|
projectDetailsParser = parseProjectDetails,
|
||||||
|
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
|
||||||
|
collectionsData = PreParsedCollections,
|
||||||
): IApplication {
|
): IApplication {
|
||||||
validateCollectionsData(collectionsData, utilities.validator);
|
validateCollectionsData(collectionsData);
|
||||||
const projectDetails = utilities.parseProjectDetails();
|
const projectDetails = projectDetailsParser(metadata);
|
||||||
const collections = collectionsData.map(
|
const collections = collectionsData.map(
|
||||||
(collection) => utilities.parseCategoryCollection(collection, projectDetails),
|
(collection) => categoryParser(collection, projectDetails),
|
||||||
);
|
);
|
||||||
const app = new Application(projectDetails, collections);
|
const app = new Application(projectDetails, collections);
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|
||||||
const PreParsedCollections: readonly CollectionData[] = [
|
export type CategoryCollectionParserType
|
||||||
|
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
|
||||||
|
|
||||||
|
const PreParsedCollections: readonly CollectionData [] = [
|
||||||
WindowsData, MacOsData, LinuxData,
|
WindowsData, MacOsData, LinuxData,
|
||||||
];
|
];
|
||||||
|
|
||||||
function validateCollectionsData(
|
function validateCollectionsData(collections: readonly CollectionData[]) {
|
||||||
collections: readonly CollectionData[],
|
if (!collections.length) {
|
||||||
validator: TypeValidator,
|
throw new Error('missing collections');
|
||||||
) {
|
}
|
||||||
validator.assertNonEmptyCollection({
|
|
||||||
value: collections,
|
|
||||||
valueName: 'Collections',
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ApplicationParserUtilities {
|
|
||||||
readonly parseCategoryCollection: CategoryCollectionParser;
|
|
||||||
readonly validator: TypeValidator;
|
|
||||||
readonly parseProjectDetails: ProjectDetailsParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: ApplicationParserUtilities = {
|
|
||||||
parseCategoryCollection,
|
|
||||||
parseProjectDetails,
|
|
||||||
validator: createTypeValidator(),
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,75 +1,34 @@
|
|||||||
import type { CollectionData } from '@/application/collections/';
|
import type { CollectionData } from '@/application/collections/';
|
||||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||||
import type { ICategoryCollection } from '@/domain/Collection/ICategoryCollection';
|
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
|
||||||
import { CategoryCollection } from '@/domain/Collection/CategoryCollection';
|
import { CategoryCollection } from '@/domain/CategoryCollection';
|
||||||
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
|
||||||
import { createEnumParser, type EnumParser } from '../Common/Enum';
|
import { createEnumParser } from '../Common/Enum';
|
||||||
import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
|
import { parseCategory } from './CategoryParser';
|
||||||
import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext';
|
||||||
import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
|
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
|
||||||
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
|
|
||||||
|
|
||||||
export const parseCategoryCollection: CategoryCollectionParser = (
|
export function parseCategoryCollection(
|
||||||
content,
|
|
||||||
projectDetails,
|
|
||||||
utilities: CategoryCollectionParserUtilities = DefaultUtilities,
|
|
||||||
) => {
|
|
||||||
validateCollection(content, utilities.validator);
|
|
||||||
const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
|
|
||||||
const collectionUtilities = utilities.createUtilities(content.functions, scripting);
|
|
||||||
const categories = content.actions.map(
|
|
||||||
(action) => utilities.parseCategory(action, collectionUtilities),
|
|
||||||
);
|
|
||||||
const os = utilities.osParser.parseEnum(content.os, 'os');
|
|
||||||
const collection = utilities.createCategoryCollection({
|
|
||||||
os, actions: categories, scripting,
|
|
||||||
});
|
|
||||||
return collection;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type CategoryCollectionFactory = (
|
|
||||||
...parameters: ConstructorParameters<typeof CategoryCollection>
|
|
||||||
) => ICategoryCollection;
|
|
||||||
|
|
||||||
export interface CategoryCollectionParser {
|
|
||||||
(
|
|
||||||
content: CollectionData,
|
|
||||||
projectDetails: ProjectDetails,
|
|
||||||
utilities?: CategoryCollectionParserUtilities,
|
|
||||||
): ICategoryCollection;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCollection(
|
|
||||||
content: CollectionData,
|
content: CollectionData,
|
||||||
validator: TypeValidator,
|
projectDetails: ProjectDetails,
|
||||||
): void {
|
osParser = createEnumParser(OperatingSystem),
|
||||||
validator.assertObject({
|
): ICategoryCollection {
|
||||||
value: content,
|
validate(content);
|
||||||
valueName: 'Collection',
|
const scripting = new ScriptingDefinitionParser()
|
||||||
allowedProperties: [
|
.parse(content.scripting, projectDetails);
|
||||||
'os', 'scripting', 'actions', 'functions',
|
const context = new CategoryCollectionParseContext(content.functions, scripting);
|
||||||
],
|
const categories = content.actions.map((action) => parseCategory(action, context));
|
||||||
});
|
const os = osParser.parseEnum(content.os, 'os');
|
||||||
validator.assertNonEmptyCollection({
|
const collection = new CategoryCollection(
|
||||||
value: content.actions,
|
os,
|
||||||
valueName: '\'actions\' in collection',
|
categories,
|
||||||
});
|
scripting,
|
||||||
|
);
|
||||||
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CategoryCollectionParserUtilities {
|
function validate(content: CollectionData): void {
|
||||||
readonly osParser: EnumParser<OperatingSystem>;
|
if (!content.actions.length) {
|
||||||
readonly validator: TypeValidator;
|
throw new Error('content does not define any action');
|
||||||
readonly parseScriptingDefinition: ScriptingDefinitionParser;
|
}
|
||||||
readonly createUtilities: CategoryCollectionSpecificUtilitiesFactory;
|
|
||||||
readonly parseCategory: CategoryParser;
|
|
||||||
readonly createCategoryCollection: CategoryCollectionFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultUtilities: CategoryCollectionParserUtilities = {
|
|
||||||
osParser: createEnumParser(OperatingSystem),
|
|
||||||
validator: createTypeValidator(),
|
|
||||||
parseScriptingDefinition,
|
|
||||||
createUtilities: createCollectionUtilities,
|
|
||||||
parseCategory,
|
|
||||||
createCategoryCollection: (...args) => new CategoryCollection(...args),
|
|
||||||
};
|
|
||||||
|
|||||||
171
src/application/Parser/CategoryParser.ts
Normal file
171
src/application/Parser/CategoryParser.ts
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
import type {
|
||||||
|
CategoryData, ScriptData, CategoryOrScriptData,
|
||||||
|
} from '@/application/collections/';
|
||||||
|
import { Script } from '@/domain/Script';
|
||||||
|
import { Category } from '@/domain/Category';
|
||||||
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
|
import type { ICategory } from '@/domain/ICategory';
|
||||||
|
import { parseDocs, type DocsParser } from './DocumentationParser';
|
||||||
|
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
||||||
|
import { createNodeDataValidator, type NodeDataValidator, type NodeDataValidatorFactory } from './NodeValidation/NodeDataValidator';
|
||||||
|
import { NodeDataType } from './NodeValidation/NodeDataType';
|
||||||
|
import type { ICategoryCollectionParseContext } from './Script/ICategoryCollectionParseContext';
|
||||||
|
|
||||||
|
let categoryIdCounter = 0;
|
||||||
|
|
||||||
|
export function parseCategory(
|
||||||
|
category: CategoryData,
|
||||||
|
context: ICategoryCollectionParseContext,
|
||||||
|
utilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
||||||
|
): Category {
|
||||||
|
return parseCategoryRecursively({
|
||||||
|
categoryData: category,
|
||||||
|
context,
|
||||||
|
utilities,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryParseContext {
|
||||||
|
readonly categoryData: CategoryData;
|
||||||
|
readonly context: ICategoryCollectionParseContext;
|
||||||
|
readonly parentCategory?: CategoryData;
|
||||||
|
readonly utilities: CategoryParserUtilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCategoryRecursively(
|
||||||
|
context: CategoryParseContext,
|
||||||
|
): Category | never {
|
||||||
|
const validator = ensureValidCategory(context);
|
||||||
|
const children: CategoryChildren = {
|
||||||
|
subcategories: new Array<Category>(),
|
||||||
|
subscripts: new Array<Script>(),
|
||||||
|
};
|
||||||
|
for (const data of context.categoryData.children) {
|
||||||
|
parseNode({
|
||||||
|
nodeData: data,
|
||||||
|
children,
|
||||||
|
parent: context.categoryData,
|
||||||
|
utilities: context.utilities,
|
||||||
|
context: context.context,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return context.utilities.createCategory({
|
||||||
|
id: categoryIdCounter++,
|
||||||
|
name: context.categoryData.category,
|
||||||
|
docs: context.utilities.parseDocs(context.categoryData),
|
||||||
|
subcategories: children.subcategories,
|
||||||
|
scripts: children.subscripts,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
throw context.utilities.wrapError(
|
||||||
|
error,
|
||||||
|
validator.createContextualErrorMessage('Failed to parse category.'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureValidCategory(
|
||||||
|
context: CategoryParseContext,
|
||||||
|
): NodeDataValidator {
|
||||||
|
const category = context.categoryData;
|
||||||
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
|
type: NodeDataType.Category,
|
||||||
|
selfNode: context.categoryData,
|
||||||
|
parentNode: context.parentCategory,
|
||||||
|
});
|
||||||
|
validator.assertDefined(category);
|
||||||
|
validator.assertValidName(category.category);
|
||||||
|
validator.assert(
|
||||||
|
() => Boolean(category.children) && category.children.length > 0,
|
||||||
|
`"${category.category}" has no children.`,
|
||||||
|
);
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface CategoryChildren {
|
||||||
|
readonly subcategories: Category[];
|
||||||
|
readonly subscripts: Script[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface NodeParseContext {
|
||||||
|
readonly nodeData: CategoryOrScriptData;
|
||||||
|
readonly children: CategoryChildren;
|
||||||
|
readonly parent: CategoryData;
|
||||||
|
readonly context: ICategoryCollectionParseContext;
|
||||||
|
|
||||||
|
readonly utilities: CategoryParserUtilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseNode(context: NodeParseContext) {
|
||||||
|
const validator: NodeDataValidator = context.utilities.createValidator({
|
||||||
|
selfNode: context.nodeData,
|
||||||
|
parentNode: context.parent,
|
||||||
|
});
|
||||||
|
validator.assertDefined(context.nodeData);
|
||||||
|
validator.assert(
|
||||||
|
() => isCategory(context.nodeData) || isScript(context.nodeData),
|
||||||
|
'Node is neither a category or a script.',
|
||||||
|
);
|
||||||
|
if (isCategory(context.nodeData)) {
|
||||||
|
const subCategory = parseCategoryRecursively({
|
||||||
|
categoryData: context.nodeData,
|
||||||
|
context: context.context,
|
||||||
|
parentCategory: context.parent,
|
||||||
|
utilities: context.utilities,
|
||||||
|
});
|
||||||
|
context.children.subcategories.push(subCategory);
|
||||||
|
} else { // A script
|
||||||
|
const script = context.utilities.parseScript(context.nodeData, context.context);
|
||||||
|
context.children.subscripts.push(script);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isScript(data: CategoryOrScriptData): data is ScriptData {
|
||||||
|
return hasCode(data) || hasCall(data);
|
||||||
|
}
|
||||||
|
|
||||||
|
function isCategory(data: CategoryOrScriptData): data is CategoryData {
|
||||||
|
return hasProperty(data, 'category');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCode(data: unknown): boolean {
|
||||||
|
return hasProperty(data, 'code');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasCall(data: unknown) {
|
||||||
|
return hasProperty(data, 'call');
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasProperty(
|
||||||
|
object: unknown,
|
||||||
|
propertyName: string,
|
||||||
|
): object is NonNullable<object> {
|
||||||
|
if (typeof object !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (object === null) { // `typeof object` is `null`
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export type CategoryFactory = (
|
||||||
|
...parameters: ConstructorParameters<typeof Category>
|
||||||
|
) => ICategory;
|
||||||
|
|
||||||
|
interface CategoryParserUtilities {
|
||||||
|
readonly createCategory: CategoryFactory;
|
||||||
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
|
readonly createValidator: NodeDataValidatorFactory;
|
||||||
|
readonly parseScript: ScriptParser;
|
||||||
|
readonly parseDocs: DocsParser;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
||||||
|
createCategory: (...parameters) => new Category(...parameters),
|
||||||
|
wrapError: wrapErrorWithAdditionalContext,
|
||||||
|
createValidator: createNodeDataValidator,
|
||||||
|
parseScript,
|
||||||
|
parseDocs,
|
||||||
|
};
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
import { CustomError } from '@/application/Common/CustomError';
|
|
||||||
import { indentText } from '@/application/Common/Text/IndentText';
|
|
||||||
|
|
||||||
export interface ErrorWithContextWrapper {
|
|
||||||
(
|
|
||||||
innerError: Error,
|
|
||||||
additionalContext: string,
|
|
||||||
): Error;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
|
||||||
innerError,
|
|
||||||
additionalContext,
|
|
||||||
) => {
|
|
||||||
if (!additionalContext) {
|
|
||||||
throw new Error('Missing additional context');
|
|
||||||
}
|
|
||||||
return new ContextualError({
|
|
||||||
innerError,
|
|
||||||
additionalContext,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class for building a detailed error trace.
|
|
||||||
*
|
|
||||||
* Alternatives considered:
|
|
||||||
* - `AggregateError`:
|
|
||||||
* Similar but not well-serialized or displayed by browsers such as Chromium (last tested v126).
|
|
||||||
* - `cause` property:
|
|
||||||
* Not displayed by all browsers (last tested v126).
|
|
||||||
* Reference: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/cause
|
|
||||||
*
|
|
||||||
* This is immutable where the constructor sets the values because using getter functions such as
|
|
||||||
* `get cause()`, `get message()` does not work on Chromium (last tested v126), but works fine on
|
|
||||||
* Firefox (last tested v127).
|
|
||||||
*/
|
|
||||||
class ContextualError extends CustomError {
|
|
||||||
constructor(public readonly context: ErrorContext) {
|
|
||||||
super(
|
|
||||||
generateDetailedErrorMessageWithContext(context),
|
|
||||||
{
|
|
||||||
cause: context.innerError,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ErrorContext {
|
|
||||||
readonly innerError: Error;
|
|
||||||
readonly additionalContext: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateDetailedErrorMessageWithContext(
|
|
||||||
context: ErrorContext,
|
|
||||||
): string {
|
|
||||||
return [
|
|
||||||
'\n',
|
|
||||||
// Display the current error message first, then the root cause.
|
|
||||||
// This prevents repetitive main messages for errors with a `cause:` chain,
|
|
||||||
// aligning with browser error display conventions.
|
|
||||||
context.additionalContext,
|
|
||||||
'\n',
|
|
||||||
'Error Trace (starting from root cause):',
|
|
||||||
indentText(
|
|
||||||
formatErrorTrace(
|
|
||||||
// Displaying contexts from the top frame (deepest, most recent) aligns with
|
|
||||||
// common debugger/compiler standard.
|
|
||||||
extractErrorTraceAscendingFromDeepest(context),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
'\n',
|
|
||||||
].join('\n');
|
|
||||||
}
|
|
||||||
|
|
||||||
function extractErrorTraceAscendingFromDeepest(
|
|
||||||
context: ErrorContext,
|
|
||||||
): string[] {
|
|
||||||
const originalError = findRootError(context.innerError);
|
|
||||||
const contextsDescendingFromMostRecent: string[] = [
|
|
||||||
context.additionalContext,
|
|
||||||
...gatherContextsFromErrorChain(context.innerError),
|
|
||||||
originalError.toString(),
|
|
||||||
];
|
|
||||||
const contextsAscendingFromDeepest = contextsDescendingFromMostRecent.reverse();
|
|
||||||
return contextsAscendingFromDeepest;
|
|
||||||
}
|
|
||||||
|
|
||||||
function findRootError(error: Error): Error {
|
|
||||||
if (error instanceof ContextualError) {
|
|
||||||
return findRootError(error.context.innerError);
|
|
||||||
}
|
|
||||||
return error;
|
|
||||||
}
|
|
||||||
|
|
||||||
function gatherContextsFromErrorChain(
|
|
||||||
error: Error,
|
|
||||||
accumulatedContexts: string[] = [],
|
|
||||||
): string[] {
|
|
||||||
if (error instanceof ContextualError) {
|
|
||||||
accumulatedContexts.push(error.context.additionalContext);
|
|
||||||
return gatherContextsFromErrorChain(error.context.innerError, accumulatedContexts);
|
|
||||||
}
|
|
||||||
return accumulatedContexts;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatErrorTrace(
|
|
||||||
errorMessages: readonly string[],
|
|
||||||
): string {
|
|
||||||
if (errorMessages.length === 1) {
|
|
||||||
return errorMessages[0];
|
|
||||||
}
|
|
||||||
return errorMessages
|
|
||||||
.map((context, index) => `${index + 1}.${indentText(context)}`)
|
|
||||||
.join('\n');
|
|
||||||
}
|
|
||||||
@@ -1,131 +0,0 @@
|
|||||||
import type { PropertyKeys } from '@/TypeHelpers';
|
|
||||||
import {
|
|
||||||
isNullOrUndefined, isArray, isPlainObject, isString,
|
|
||||||
} from '@/TypeHelpers';
|
|
||||||
|
|
||||||
export interface TypeValidator {
|
|
||||||
assertObject<T>(assertion: ObjectAssertion<T>): void;
|
|
||||||
assertNonEmptyCollection(assertion: NonEmptyCollectionAssertion): void;
|
|
||||||
assertNonEmptyString(assertion: NonEmptyStringAssertion): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NonEmptyCollectionAssertion {
|
|
||||||
readonly value: unknown;
|
|
||||||
readonly valueName: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface RegexValidationRule {
|
|
||||||
readonly expectedMatch: RegExp;
|
|
||||||
readonly errorMessage: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NonEmptyStringAssertion {
|
|
||||||
readonly value: unknown;
|
|
||||||
readonly valueName: string;
|
|
||||||
readonly rule?: RegexValidationRule;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ObjectAssertion<T> {
|
|
||||||
readonly value: T | unknown;
|
|
||||||
readonly valueName: string;
|
|
||||||
readonly allowedProperties?: readonly PropertyKeys<T>[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export function createTypeValidator(): TypeValidator {
|
|
||||||
return {
|
|
||||||
assertObject: (assertion) => {
|
|
||||||
assertDefined(assertion.value, assertion.valueName);
|
|
||||||
assertPlainObject(assertion.value, assertion.valueName);
|
|
||||||
assertNoEmptyProperties(assertion.value, assertion.valueName);
|
|
||||||
if (assertion.allowedProperties !== undefined) {
|
|
||||||
const allowedProperties = assertion.allowedProperties.map((p) => p as string);
|
|
||||||
assertAllowedProperties(assertion.value, assertion.valueName, allowedProperties);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
assertNonEmptyCollection: (assertion) => {
|
|
||||||
assertDefined(assertion.value, assertion.valueName);
|
|
||||||
assertArray(assertion.value, assertion.valueName);
|
|
||||||
assertNonEmpty(assertion.value, assertion.valueName);
|
|
||||||
},
|
|
||||||
assertNonEmptyString: (assertion) => {
|
|
||||||
assertDefined(assertion.value, assertion.valueName);
|
|
||||||
assertString(assertion.value, assertion.valueName);
|
|
||||||
if (assertion.value.length === 0) {
|
|
||||||
throw new Error(`'${assertion.valueName}' is missing.`);
|
|
||||||
}
|
|
||||||
if (assertion.rule) {
|
|
||||||
if (!assertion.value.match(assertion.rule.expectedMatch)) {
|
|
||||||
throw new Error(assertion.rule.errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertDefined<T>(
|
|
||||||
value: T,
|
|
||||||
valueName: string,
|
|
||||||
): asserts value is NonNullable<T> {
|
|
||||||
if (isNullOrUndefined(value)) {
|
|
||||||
throw new Error(`'${valueName}' is missing.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertPlainObject(
|
|
||||||
value: unknown,
|
|
||||||
valueName: string,
|
|
||||||
): asserts value is object {
|
|
||||||
if (!isPlainObject(value)) {
|
|
||||||
throw new Error(`'${valueName}' is not an object.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNoEmptyProperties(
|
|
||||||
value: object,
|
|
||||||
valueName: string,
|
|
||||||
): void {
|
|
||||||
if (Object.keys(value).length === 0) {
|
|
||||||
throw new Error(`'${valueName}' is an empty object without properties.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertAllowedProperties(
|
|
||||||
value: object,
|
|
||||||
valueName: string,
|
|
||||||
allowedProperties: readonly string[],
|
|
||||||
): void {
|
|
||||||
const properties = Object.keys(value).map((p) => p as string);
|
|
||||||
const disallowedProperties = properties.filter(
|
|
||||||
(prop) => !allowedProperties.map((p) => p as string).includes(prop),
|
|
||||||
);
|
|
||||||
if (disallowedProperties.length > 0) {
|
|
||||||
throw new Error(`'${valueName}' has disallowed properties: ${disallowedProperties.join(', ')}.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertArray(
|
|
||||||
value: unknown,
|
|
||||||
valueName: string,
|
|
||||||
): asserts value is Array<unknown> {
|
|
||||||
if (!isArray(value)) {
|
|
||||||
throw new Error(`${valueName} should be of type 'array', but is of type '${typeof value}'.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertString(
|
|
||||||
value: unknown,
|
|
||||||
valueName: string,
|
|
||||||
): asserts value is string {
|
|
||||||
if (!isString(value)) {
|
|
||||||
throw new Error(`${valueName} should be of type 'string', but is of type '${typeof value}'.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNonEmpty(
|
|
||||||
value: Array<unknown>,
|
|
||||||
valueName: string,
|
|
||||||
): void {
|
|
||||||
if (value.length === 0) {
|
|
||||||
throw new Error(`'${valueName}' cannot be an empty array.`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
42
src/application/Parser/ContextualError.ts
Normal file
42
src/application/Parser/ContextualError.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { CustomError } from '@/application/Common/CustomError';
|
||||||
|
|
||||||
|
export interface ErrorWithContextWrapper {
|
||||||
|
(
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
): Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const wrapErrorWithAdditionalContext: ErrorWithContextWrapper = (
|
||||||
|
error: Error,
|
||||||
|
additionalContext: string,
|
||||||
|
) => {
|
||||||
|
return (error instanceof ContextualError ? error : new ContextualError(error))
|
||||||
|
.withAdditionalContext(additionalContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/* AggregateError is similar but isn't well-serialized or displayed by browsers */
|
||||||
|
class ContextualError extends CustomError {
|
||||||
|
private readonly additionalContext = new Array<string>();
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public readonly innerError: Error,
|
||||||
|
) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
public withAdditionalContext(additionalContext: string): this {
|
||||||
|
this.additionalContext.push(additionalContext);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get message(): string { // toString() is not used when Chromium logs it on console
|
||||||
|
return [
|
||||||
|
'\n',
|
||||||
|
this.innerError.message,
|
||||||
|
'\n',
|
||||||
|
'Additional context:',
|
||||||
|
...this.additionalContext.map((context, index) => `${index + 1}: ${context}`),
|
||||||
|
].join('\n');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,5 +50,5 @@ class DocumentationContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function throwInvalidType(): never {
|
function throwInvalidType(): never {
|
||||||
throw new Error('docs field (documentation) must be a single string or an array of strings.');
|
throw new Error('docs field (documentation) must be an array of strings');
|
||||||
}
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
|
||||||
import type { FunctionData } from '@/application/collections/';
|
|
||||||
import { ScriptCompiler } from './Script/Compiler/ScriptCompiler';
|
|
||||||
import { SyntaxFactory } from './Script/Validation/Syntax/SyntaxFactory';
|
|
||||||
import type { IScriptCompiler } from './Script/Compiler/IScriptCompiler';
|
|
||||||
import type { ILanguageSyntax } from './Script/Validation/Syntax/ILanguageSyntax';
|
|
||||||
import type { ISyntaxFactory } from './Script/Validation/Syntax/ISyntaxFactory';
|
|
||||||
|
|
||||||
export interface CategoryCollectionSpecificUtilities {
|
|
||||||
readonly compiler: IScriptCompiler;
|
|
||||||
readonly syntax: ILanguageSyntax;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createCollectionUtilities: CategoryCollectionSpecificUtilitiesFactory = (
|
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
scripting: IScriptingDefinition,
|
|
||||||
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
|
||||||
) => {
|
|
||||||
const syntax = syntaxFactory.create(scripting.language);
|
|
||||||
return {
|
|
||||||
compiler: new ScriptCompiler({
|
|
||||||
functions: functionsData ?? [],
|
|
||||||
syntax,
|
|
||||||
}),
|
|
||||||
syntax,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CategoryCollectionSpecificUtilitiesFactory {
|
|
||||||
(
|
|
||||||
functionsData: ReadonlyArray<FunctionData> | undefined,
|
|
||||||
scripting: IScriptingDefinition,
|
|
||||||
syntaxFactory?: ISyntaxFactory,
|
|
||||||
): CategoryCollectionSpecificUtilities;
|
|
||||||
}
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import type {
|
|
||||||
CategoryData, ScriptData, ExecutableData,
|
|
||||||
} from '@/application/collections/';
|
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
|
||||||
import type { Category } from '@/domain/Executables/Category/Category';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import { createCategory, type CategoryFactory } from '@/domain/Executables/Category/CategoryFactory';
|
|
||||||
import { parseDocs, type DocsParser } from './DocumentationParser';
|
|
||||||
import { parseScript, type ScriptParser } from './Script/ScriptParser';
|
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from './Validation/ExecutableValidator';
|
|
||||||
import { ExecutableType } from './Validation/ExecutableType';
|
|
||||||
import type { CategoryCollectionSpecificUtilities } from './CategoryCollectionSpecificUtilities';
|
|
||||||
|
|
||||||
export const parseCategory: CategoryParser = (
|
|
||||||
category: CategoryData,
|
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
|
||||||
categoryUtilities: CategoryParserUtilities = DefaultCategoryParserUtilities,
|
|
||||||
) => {
|
|
||||||
return parseCategoryRecursively({
|
|
||||||
categoryData: category,
|
|
||||||
collectionUtilities,
|
|
||||||
categoryUtilities,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export interface CategoryParser {
|
|
||||||
(
|
|
||||||
category: CategoryData,
|
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
|
||||||
categoryUtilities?: CategoryParserUtilities,
|
|
||||||
): Category;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategoryParseContext {
|
|
||||||
readonly categoryData: CategoryData;
|
|
||||||
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
|
||||||
readonly parentCategory?: CategoryData;
|
|
||||||
readonly categoryUtilities: CategoryParserUtilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCategoryRecursively(
|
|
||||||
context: CategoryParseContext,
|
|
||||||
): Category | never {
|
|
||||||
const validator = ensureValidCategory(context);
|
|
||||||
const children: CategoryChildren = {
|
|
||||||
subcategories: new Array<Category>(),
|
|
||||||
subscripts: new Array<Script>(),
|
|
||||||
};
|
|
||||||
for (const data of context.categoryData.children) {
|
|
||||||
parseUnknownExecutable({
|
|
||||||
data,
|
|
||||||
children,
|
|
||||||
parent: context.categoryData,
|
|
||||||
categoryUtilities: context.categoryUtilities,
|
|
||||||
collectionUtilities: context.collectionUtilities,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
return context.categoryUtilities.createCategory({
|
|
||||||
executableId: context.categoryData.category, // Pseudo-ID for uniqueness until real ID support
|
|
||||||
name: context.categoryData.category,
|
|
||||||
docs: context.categoryUtilities.parseDocs(context.categoryData),
|
|
||||||
subcategories: children.subcategories,
|
|
||||||
scripts: children.subscripts,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw context.categoryUtilities.wrapError(
|
|
||||||
error,
|
|
||||||
validator.createContextualErrorMessage('Failed to parse category.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function ensureValidCategory(
|
|
||||||
context: CategoryParseContext,
|
|
||||||
): ExecutableValidator {
|
|
||||||
const category = context.categoryData;
|
|
||||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
|
||||||
type: ExecutableType.Category,
|
|
||||||
self: context.categoryData,
|
|
||||||
parentCategory: context.parentCategory,
|
|
||||||
});
|
|
||||||
validator.assertType((v) => v.assertObject({
|
|
||||||
value: category,
|
|
||||||
valueName: `Category '${category.category}'` ?? 'Category',
|
|
||||||
allowedProperties: [
|
|
||||||
'docs', 'children', 'category',
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
validator.assertValidName(category.category);
|
|
||||||
validator.assertType((v) => v.assertNonEmptyCollection({
|
|
||||||
value: category.children,
|
|
||||||
valueName: category.category,
|
|
||||||
}));
|
|
||||||
return validator;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategoryChildren {
|
|
||||||
readonly subcategories: Category[];
|
|
||||||
readonly subscripts: Script[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ExecutableParseContext {
|
|
||||||
readonly data: ExecutableData;
|
|
||||||
readonly children: CategoryChildren;
|
|
||||||
readonly parent: CategoryData;
|
|
||||||
readonly collectionUtilities: CategoryCollectionSpecificUtilities;
|
|
||||||
readonly categoryUtilities: CategoryParserUtilities;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseUnknownExecutable(context: ExecutableParseContext) {
|
|
||||||
const validator: ExecutableValidator = context.categoryUtilities.createValidator({
|
|
||||||
self: context.data,
|
|
||||||
parentCategory: context.parent,
|
|
||||||
});
|
|
||||||
validator.assertType((v) => v.assertObject({
|
|
||||||
value: context.data,
|
|
||||||
valueName: 'Executable',
|
|
||||||
}));
|
|
||||||
validator.assert(
|
|
||||||
() => isCategory(context.data) || isScript(context.data),
|
|
||||||
'Executable is neither a category or a script.',
|
|
||||||
);
|
|
||||||
if (isCategory(context.data)) {
|
|
||||||
const subCategory = parseCategoryRecursively({
|
|
||||||
categoryData: context.data,
|
|
||||||
collectionUtilities: context.collectionUtilities,
|
|
||||||
parentCategory: context.parent,
|
|
||||||
categoryUtilities: context.categoryUtilities,
|
|
||||||
});
|
|
||||||
context.children.subcategories.push(subCategory);
|
|
||||||
} else { // A script
|
|
||||||
const script = context.categoryUtilities.parseScript(context.data, context.collectionUtilities);
|
|
||||||
context.children.subscripts.push(script);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isScript(data: ExecutableData): data is ScriptData {
|
|
||||||
return hasCode(data) || hasCall(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCategory(data: ExecutableData): data is CategoryData {
|
|
||||||
return hasProperty(data, 'category');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCode(data: unknown): boolean {
|
|
||||||
return hasProperty(data, 'code');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCall(data: unknown) {
|
|
||||||
return hasProperty(data, 'call');
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasProperty(
|
|
||||||
object: unknown,
|
|
||||||
propertyName: string,
|
|
||||||
): object is NonNullable<object> {
|
|
||||||
if (typeof object !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (object === null) { // `typeof object` is `null`
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return Object.prototype.hasOwnProperty.call(object, propertyName);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CategoryParserUtilities {
|
|
||||||
readonly createCategory: CategoryFactory;
|
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
|
||||||
readonly createValidator: ExecutableValidatorFactory;
|
|
||||||
readonly parseScript: ScriptParser;
|
|
||||||
readonly parseDocs: DocsParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
|
|
||||||
createCategory,
|
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
|
||||||
createValidator: createExecutableDataValidator,
|
|
||||||
parseScript,
|
|
||||||
parseDocs,
|
|
||||||
};
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
|
||||||
import { validateParameterName, type ParameterNameValidator } from '@/application/Parser/Executable/Script/Compiler/Function/Shared/ParameterNameValidator';
|
|
||||||
|
|
||||||
export interface FunctionCallArgument {
|
|
||||||
readonly parameterName: string;
|
|
||||||
readonly argumentValue: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FunctionCallArgumentFactory {
|
|
||||||
(
|
|
||||||
parameterName: string,
|
|
||||||
argumentValue: string,
|
|
||||||
utilities?: FunctionCallArgumentFactoryUtilities,
|
|
||||||
): FunctionCallArgument;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createFunctionCallArgument: FunctionCallArgumentFactory = (
|
|
||||||
parameterName: string,
|
|
||||||
argumentValue: string,
|
|
||||||
utilities: FunctionCallArgumentFactoryUtilities = DefaultUtilities,
|
|
||||||
): FunctionCallArgument => {
|
|
||||||
utilities.validateParameterName(parameterName);
|
|
||||||
utilities.typeValidator.assertNonEmptyString({
|
|
||||||
value: argumentValue,
|
|
||||||
valueName: `Function parameter '${parameterName}'`,
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
parameterName,
|
|
||||||
argumentValue,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
interface FunctionCallArgumentFactoryUtilities {
|
|
||||||
readonly typeValidator: TypeValidator;
|
|
||||||
readonly validateParameterName: ParameterNameValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: FunctionCallArgumentFactoryUtilities = {
|
|
||||||
typeValidator: createTypeValidator(),
|
|
||||||
validateParameterName,
|
|
||||||
};
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import type {
|
|
||||||
FunctionCallData,
|
|
||||||
FunctionCallsData,
|
|
||||||
FunctionCallParametersData,
|
|
||||||
} from '@/application/collections/';
|
|
||||||
import { isArray, isPlainObject } from '@/TypeHelpers';
|
|
||||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
|
||||||
import { FunctionCallArgumentCollection } from './Argument/FunctionCallArgumentCollection';
|
|
||||||
import { ParsedFunctionCall } from './ParsedFunctionCall';
|
|
||||||
import { createFunctionCallArgument, type FunctionCallArgumentFactory } from './Argument/FunctionCallArgument';
|
|
||||||
import type { FunctionCall } from './FunctionCall';
|
|
||||||
|
|
||||||
export interface FunctionCallsParser {
|
|
||||||
(
|
|
||||||
calls: FunctionCallsData,
|
|
||||||
utilities?: FunctionCallParsingUtilities,
|
|
||||||
): FunctionCall[];
|
|
||||||
}
|
|
||||||
|
|
||||||
interface FunctionCallParsingUtilities {
|
|
||||||
readonly typeValidator: TypeValidator;
|
|
||||||
readonly createCallArgument: FunctionCallArgumentFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: FunctionCallParsingUtilities = {
|
|
||||||
typeValidator: createTypeValidator(),
|
|
||||||
createCallArgument: createFunctionCallArgument,
|
|
||||||
};
|
|
||||||
|
|
||||||
export const parseFunctionCalls: FunctionCallsParser = (
|
|
||||||
calls,
|
|
||||||
utilities = DefaultUtilities,
|
|
||||||
) => {
|
|
||||||
const sequence = getCallSequence(calls, utilities.typeValidator);
|
|
||||||
return sequence.map((call) => parseFunctionCall(call, utilities));
|
|
||||||
};
|
|
||||||
|
|
||||||
function getCallSequence(calls: FunctionCallsData, validator: TypeValidator): FunctionCallData[] {
|
|
||||||
if (!isPlainObject(calls) && !isArray(calls)) {
|
|
||||||
throw new Error('called function(s) must be an object or array');
|
|
||||||
}
|
|
||||||
if (isArray(calls)) {
|
|
||||||
validator.assertNonEmptyCollection({
|
|
||||||
value: calls,
|
|
||||||
valueName: 'Function call sequence',
|
|
||||||
});
|
|
||||||
return calls as FunctionCallData[];
|
|
||||||
}
|
|
||||||
const singleCall = calls as FunctionCallData;
|
|
||||||
return [singleCall];
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseFunctionCall(
|
|
||||||
call: FunctionCallData,
|
|
||||||
utilities: FunctionCallParsingUtilities,
|
|
||||||
): FunctionCall {
|
|
||||||
utilities.typeValidator.assertObject({
|
|
||||||
value: call,
|
|
||||||
valueName: 'Function call',
|
|
||||||
allowedProperties: ['function', 'parameters'],
|
|
||||||
});
|
|
||||||
const callArgs = parseArgs(call.parameters, utilities.createCallArgument);
|
|
||||||
return new ParsedFunctionCall(call.function, callArgs);
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseArgs(
|
|
||||||
parameters: FunctionCallParametersData | undefined,
|
|
||||||
createArgument: FunctionCallArgumentFactory,
|
|
||||||
): FunctionCallArgumentCollection {
|
|
||||||
const parametersMap = parameters ?? {};
|
|
||||||
return Object.keys(parametersMap)
|
|
||||||
.map((parameterName) => {
|
|
||||||
const argumentValue = parametersMap[parameterName];
|
|
||||||
return createArgument(parameterName, argumentValue);
|
|
||||||
})
|
|
||||||
.reduce((args, arg) => {
|
|
||||||
args.addArgument(arg);
|
|
||||||
return args;
|
|
||||||
}, new FunctionCallArgumentCollection());
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
import type { ParameterDefinitionData } from '@/application/collections/';
|
|
||||||
import { validateParameterName, type ParameterNameValidator } from '../Shared/ParameterNameValidator';
|
|
||||||
import type { FunctionParameter } from './FunctionParameter';
|
|
||||||
|
|
||||||
export interface FunctionParameterParser {
|
|
||||||
(
|
|
||||||
data: ParameterDefinitionData,
|
|
||||||
validator?: ParameterNameValidator,
|
|
||||||
): FunctionParameter;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseFunctionParameter: FunctionParameterParser = (
|
|
||||||
data,
|
|
||||||
validator = validateParameterName,
|
|
||||||
) => {
|
|
||||||
validator(data.name);
|
|
||||||
return {
|
|
||||||
name: data.name,
|
|
||||||
isOptional: data.optional || false,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import type { FunctionParameter } from './FunctionParameter';
|
|
||||||
|
|
||||||
export interface IReadOnlyFunctionParameterCollection {
|
|
||||||
readonly all: readonly FunctionParameter[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
|
|
||||||
addParameter(parameter: FunctionParameter): void;
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
import { createTypeValidator, type TypeValidator } from '@/application/Parser/Common/TypeValidator';
|
|
||||||
|
|
||||||
export interface ParameterNameValidator {
|
|
||||||
(
|
|
||||||
parameterName: string,
|
|
||||||
typeValidator?: TypeValidator,
|
|
||||||
): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const validateParameterName = (
|
|
||||||
parameterName: string,
|
|
||||||
typeValidator = createTypeValidator(),
|
|
||||||
) => {
|
|
||||||
typeValidator.assertNonEmptyString({
|
|
||||||
value: parameterName,
|
|
||||||
valueName: 'Parameter name',
|
|
||||||
rule: {
|
|
||||||
expectedMatch: /^[0-9a-zA-Z]+$/,
|
|
||||||
errorMessage: `parameter name must be alphanumeric but it was "${parameterName}".`,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
import type { FunctionData, ScriptData, CallInstruction } from '@/application/collections/';
|
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
|
||||||
import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
|
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
|
||||||
import { createScriptCode, type ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
|
||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import { FunctionCallSequenceCompiler } from './Function/Call/Compiler/FunctionCallSequenceCompiler';
|
|
||||||
import { parseFunctionCalls } from './Function/Call/FunctionCallsParser';
|
|
||||||
import { parseSharedFunctions, type SharedFunctionsParser } from './Function/SharedFunctionsParser';
|
|
||||||
import type { CompiledCode } from './Function/Call/Compiler/CompiledCode';
|
|
||||||
import type { IScriptCompiler } from './IScriptCompiler';
|
|
||||||
import type { ISharedFunctionCollection } from './Function/ISharedFunctionCollection';
|
|
||||||
import type { FunctionCallCompiler } from './Function/Call/Compiler/FunctionCallCompiler';
|
|
||||||
|
|
||||||
interface ScriptCompilerUtilities {
|
|
||||||
readonly sharedFunctionsParser: SharedFunctionsParser;
|
|
||||||
readonly callCompiler: FunctionCallCompiler;
|
|
||||||
readonly codeValidator: ICodeValidator;
|
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
|
||||||
readonly scriptCodeFactory: ScriptCodeFactory;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: ScriptCompilerUtilities = {
|
|
||||||
sharedFunctionsParser: parseSharedFunctions,
|
|
||||||
callCompiler: FunctionCallSequenceCompiler.instance,
|
|
||||||
codeValidator: CodeValidator.instance,
|
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
|
||||||
scriptCodeFactory: createScriptCode,
|
|
||||||
};
|
|
||||||
|
|
||||||
interface CategoryCollectionDataContext {
|
|
||||||
readonly functions: readonly FunctionData[];
|
|
||||||
readonly syntax: ILanguageSyntax;
|
|
||||||
}
|
|
||||||
|
|
||||||
export class ScriptCompiler implements IScriptCompiler {
|
|
||||||
private readonly functions: ISharedFunctionCollection;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
categoryContext: CategoryCollectionDataContext,
|
|
||||||
private readonly utilities: ScriptCompilerUtilities = DefaultUtilities,
|
|
||||||
) {
|
|
||||||
this.functions = this.utilities.sharedFunctionsParser(
|
|
||||||
categoryContext.functions,
|
|
||||||
categoryContext.syntax,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public canCompile(script: ScriptData): boolean {
|
|
||||||
return hasCall(script);
|
|
||||||
}
|
|
||||||
|
|
||||||
public compile(script: ScriptData): ScriptCode {
|
|
||||||
try {
|
|
||||||
if (!hasCall(script)) {
|
|
||||||
throw new Error('Script does include any calls.');
|
|
||||||
}
|
|
||||||
const calls = parseFunctionCalls(script.call);
|
|
||||||
const compiledCode = this.utilities.callCompiler.compileFunctionCalls(calls, this.functions);
|
|
||||||
validateCompiledCode(compiledCode, this.utilities.codeValidator);
|
|
||||||
return this.utilities.scriptCodeFactory(
|
|
||||||
compiledCode.code,
|
|
||||||
compiledCode.revertCode,
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
throw this.utilities.wrapError(error, `Failed to compile script: ${script.name}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateCompiledCode(compiledCode: CompiledCode, validator: ICodeValidator): void {
|
|
||||||
filterEmptyStrings([compiledCode.code, compiledCode.revertCode])
|
|
||||||
.forEach(
|
|
||||||
(code) => validator.throwIfInvalid(
|
|
||||||
code,
|
|
||||||
[new NoEmptyLines()],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
|
|
||||||
return (data as CallInstruction).call !== undefined;
|
|
||||||
}
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
import type { ScriptData, CodeScriptData, CallScriptData } from '@/application/collections/';
|
|
||||||
import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
|
|
||||||
import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
|
|
||||||
import { RecommendationLevel } from '@/domain/Executables/Script/RecommendationLevel';
|
|
||||||
import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
|
|
||||||
import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
|
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
|
||||||
import type { ScriptCodeFactory } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
|
||||||
import { createScriptCode } from '@/domain/Executables/Script/Code/ScriptCodeFactory';
|
|
||||||
import type { Script } from '@/domain/Executables/Script/Script';
|
|
||||||
import { createEnumParser, type EnumParser } from '@/application/Common/Enum';
|
|
||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import { createScript, type ScriptFactory } from '@/domain/Executables/Script/ScriptFactory';
|
|
||||||
import { parseDocs, type DocsParser } from '../DocumentationParser';
|
|
||||||
import { ExecutableType } from '../Validation/ExecutableType';
|
|
||||||
import { createExecutableDataValidator, type ExecutableValidator, type ExecutableValidatorFactory } from '../Validation/ExecutableValidator';
|
|
||||||
import { CodeValidator } from './Validation/CodeValidator';
|
|
||||||
import { NoDuplicatedLines } from './Validation/Rules/NoDuplicatedLines';
|
|
||||||
import type { CategoryCollectionSpecificUtilities } from '../CategoryCollectionSpecificUtilities';
|
|
||||||
|
|
||||||
export interface ScriptParser {
|
|
||||||
(
|
|
||||||
data: ScriptData,
|
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
|
||||||
scriptUtilities?: ScriptParserUtilities,
|
|
||||||
): Script;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const parseScript: ScriptParser = (
|
|
||||||
data,
|
|
||||||
collectionUtilities,
|
|
||||||
scriptUtilities = DefaultUtilities,
|
|
||||||
) => {
|
|
||||||
const validator = scriptUtilities.createValidator({
|
|
||||||
type: ExecutableType.Script,
|
|
||||||
self: data,
|
|
||||||
});
|
|
||||||
validateScript(data, validator);
|
|
||||||
try {
|
|
||||||
const script = scriptUtilities.createScript({
|
|
||||||
executableId: data.name, // Pseudo-ID for uniqueness until real ID support
|
|
||||||
name: data.name,
|
|
||||||
code: parseCode(
|
|
||||||
data,
|
|
||||||
collectionUtilities,
|
|
||||||
scriptUtilities.codeValidator,
|
|
||||||
scriptUtilities.createCode,
|
|
||||||
),
|
|
||||||
docs: scriptUtilities.parseDocs(data),
|
|
||||||
level: parseLevel(data.recommend, scriptUtilities.levelParser),
|
|
||||||
});
|
|
||||||
return script;
|
|
||||||
} catch (error) {
|
|
||||||
throw scriptUtilities.wrapError(
|
|
||||||
error,
|
|
||||||
validator.createContextualErrorMessage('Failed to parse script.'),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function parseLevel(
|
|
||||||
level: string | undefined,
|
|
||||||
parser: EnumParser<RecommendationLevel>,
|
|
||||||
): RecommendationLevel | undefined {
|
|
||||||
if (!level) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
return parser.parseEnum(level, 'level');
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseCode(
|
|
||||||
script: ScriptData,
|
|
||||||
collectionUtilities: CategoryCollectionSpecificUtilities,
|
|
||||||
codeValidator: ICodeValidator,
|
|
||||||
createCode: ScriptCodeFactory,
|
|
||||||
): ScriptCode {
|
|
||||||
if (collectionUtilities.compiler.canCompile(script)) {
|
|
||||||
return collectionUtilities.compiler.compile(script);
|
|
||||||
}
|
|
||||||
const codeScript = script as CodeScriptData; // Must be inline code if it cannot be compiled
|
|
||||||
const code = createCode(codeScript.code, codeScript.revertCode);
|
|
||||||
validateHardcodedCodeWithoutCalls(code, codeValidator, collectionUtilities.syntax);
|
|
||||||
return code;
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateHardcodedCodeWithoutCalls(
|
|
||||||
scriptCode: ScriptCode,
|
|
||||||
validator: ICodeValidator,
|
|
||||||
syntax: ILanguageSyntax,
|
|
||||||
) {
|
|
||||||
filterEmptyStrings([scriptCode.execute, scriptCode.revert])
|
|
||||||
.forEach(
|
|
||||||
(code) => validator.throwIfInvalid(
|
|
||||||
code,
|
|
||||||
[new NoEmptyLines(), new NoDuplicatedLines(syntax)],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function validateScript(
|
|
||||||
script: ScriptData,
|
|
||||||
validator: ExecutableValidator,
|
|
||||||
): asserts script is NonNullable<ScriptData> {
|
|
||||||
validator.assertType((v) => v.assertObject<CallScriptData & CodeScriptData>({
|
|
||||||
value: script,
|
|
||||||
valueName: `Script '${script.name}'` ?? 'Script',
|
|
||||||
allowedProperties: [
|
|
||||||
'name', 'recommend', 'code', 'revertCode', 'call', 'docs',
|
|
||||||
],
|
|
||||||
}));
|
|
||||||
validator.assertValidName(script.name);
|
|
||||||
validator.assert(
|
|
||||||
() => Boolean((script as CodeScriptData).code || (script as CallScriptData).call),
|
|
||||||
'Neither "call" or "code" is defined.',
|
|
||||||
);
|
|
||||||
validator.assert(
|
|
||||||
() => !((script as CodeScriptData).code && (script as CallScriptData).call),
|
|
||||||
'Both "call" and "code" are defined.',
|
|
||||||
);
|
|
||||||
validator.assert(
|
|
||||||
() => !((script as CodeScriptData).revertCode && (script as CallScriptData).call),
|
|
||||||
'Both "call" and "revertCode" are defined.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ScriptParserUtilities {
|
|
||||||
readonly levelParser: EnumParser<RecommendationLevel>;
|
|
||||||
readonly createScript: ScriptFactory;
|
|
||||||
readonly codeValidator: ICodeValidator;
|
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
|
||||||
readonly createValidator: ExecutableValidatorFactory;
|
|
||||||
readonly createCode: ScriptCodeFactory;
|
|
||||||
readonly parseDocs: DocsParser;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DefaultUtilities: ScriptParserUtilities = {
|
|
||||||
levelParser: createEnumParser(RecommendationLevel),
|
|
||||||
createScript,
|
|
||||||
codeValidator: CodeValidator.instance,
|
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
|
||||||
createValidator: createExecutableDataValidator,
|
|
||||||
createCode: createScriptCode,
|
|
||||||
parseDocs,
|
|
||||||
};
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
import type { CategoryData, ScriptData, ExecutableData } from '@/application/collections/';
|
|
||||||
import { ExecutableType } from './ExecutableType';
|
|
||||||
|
|
||||||
export type ExecutableErrorContext = {
|
|
||||||
readonly parentCategory?: CategoryData;
|
|
||||||
} & (CategoryErrorContext | ScriptErrorContext | UnknownExecutableErrorContext);
|
|
||||||
|
|
||||||
export type CategoryErrorContext = {
|
|
||||||
readonly type: ExecutableType.Category;
|
|
||||||
readonly self: CategoryData;
|
|
||||||
readonly parentCategory?: CategoryData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type ScriptErrorContext = {
|
|
||||||
readonly type: ExecutableType.Script;
|
|
||||||
readonly self: ScriptData;
|
|
||||||
readonly parentCategory?: CategoryData;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type UnknownExecutableErrorContext = {
|
|
||||||
readonly type?: undefined;
|
|
||||||
readonly self: ExecutableData;
|
|
||||||
readonly parentCategory?: CategoryData;
|
|
||||||
};
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import type { ExecutableData } from '@/application/collections/';
|
|
||||||
import { ExecutableType } from './ExecutableType';
|
|
||||||
import type { ExecutableErrorContext } from './ExecutableErrorContext';
|
|
||||||
|
|
||||||
export interface ExecutableContextErrorMessageCreator {
|
|
||||||
(
|
|
||||||
errorMessage: string,
|
|
||||||
context: ExecutableErrorContext,
|
|
||||||
): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createExecutableContextErrorMessage: ExecutableContextErrorMessageCreator = (
|
|
||||||
errorMessage,
|
|
||||||
context,
|
|
||||||
) => {
|
|
||||||
let message = '';
|
|
||||||
if (context.type !== undefined) {
|
|
||||||
message += `${ExecutableType[context.type]}: `;
|
|
||||||
}
|
|
||||||
message += errorMessage;
|
|
||||||
message += `\n\n${getErrorContextDetails(context)}`;
|
|
||||||
return message;
|
|
||||||
};
|
|
||||||
|
|
||||||
function getErrorContextDetails(context: ExecutableErrorContext): string {
|
|
||||||
let output = `Executable: ${formatExecutable(context.self)}`;
|
|
||||||
if (context.parentCategory) {
|
|
||||||
output += `\n\nParent category: ${formatExecutable(context.parentCategory)}`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatExecutable(executable: ExecutableData): string {
|
|
||||||
if (!executable) {
|
|
||||||
return 'Executable data is missing.';
|
|
||||||
}
|
|
||||||
const maxLength = 1000;
|
|
||||||
let output = JSON.stringify(executable, undefined, 2);
|
|
||||||
if (output.length > maxLength) {
|
|
||||||
output = `${output.substring(0, maxLength)}\n... [Rest of the executable trimmed]`;
|
|
||||||
}
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export enum ExecutableType {
|
|
||||||
Script,
|
|
||||||
Category,
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import { isString } from '@/TypeHelpers';
|
|
||||||
import { createTypeValidator, type TypeValidator } from '../../Common/TypeValidator';
|
|
||||||
import { type ExecutableErrorContext } from './ExecutableErrorContext';
|
|
||||||
import { createExecutableContextErrorMessage, type ExecutableContextErrorMessageCreator } from './ExecutableErrorContextMessage';
|
|
||||||
|
|
||||||
export interface ExecutableValidatorFactory {
|
|
||||||
(context: ExecutableErrorContext): ExecutableValidator;
|
|
||||||
}
|
|
||||||
|
|
||||||
type AssertTypeFunction = (validator: TypeValidator) => void;
|
|
||||||
|
|
||||||
export interface ExecutableValidator {
|
|
||||||
assertValidName(nameValue: string): void;
|
|
||||||
assertType(assert: AssertTypeFunction): void;
|
|
||||||
assert(
|
|
||||||
validationPredicate: () => boolean,
|
|
||||||
errorMessage: string,
|
|
||||||
): asserts validationPredicate is (() => true);
|
|
||||||
createContextualErrorMessage(errorMessage: string): string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createExecutableDataValidator
|
|
||||||
: ExecutableValidatorFactory = (context) => new ContextualExecutableValidator(context);
|
|
||||||
|
|
||||||
export class ContextualExecutableValidator implements ExecutableValidator {
|
|
||||||
constructor(
|
|
||||||
private readonly context: ExecutableErrorContext,
|
|
||||||
private readonly createErrorMessage
|
|
||||||
: ExecutableContextErrorMessageCreator = createExecutableContextErrorMessage,
|
|
||||||
private readonly validator: TypeValidator = createTypeValidator(),
|
|
||||||
) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertValidName(nameValue: string): void {
|
|
||||||
this.assert(() => Boolean(nameValue), 'missing name');
|
|
||||||
this.assert(
|
|
||||||
() => isString(nameValue),
|
|
||||||
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
public assertType(assert: AssertTypeFunction): void {
|
|
||||||
try {
|
|
||||||
assert(this.validator);
|
|
||||||
} catch (error) {
|
|
||||||
this.throw(error.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public assert(
|
|
||||||
validationPredicate: () => boolean,
|
|
||||||
errorMessage: string,
|
|
||||||
): asserts validationPredicate is (() => true) {
|
|
||||||
if (!validationPredicate()) {
|
|
||||||
this.throw(errorMessage);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public createContextualErrorMessage(errorMessage: string): string {
|
|
||||||
return this.createErrorMessage(errorMessage, this.context);
|
|
||||||
}
|
|
||||||
|
|
||||||
private throw(errorMessage: string): never {
|
|
||||||
throw new Error(
|
|
||||||
this.createContextualErrorMessage(errorMessage),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
3
src/application/Parser/NodeValidation/NodeData.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { ScriptData, CategoryData } from '@/application/collections/';
|
||||||
|
|
||||||
|
export type NodeData = CategoryData | ScriptData;
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import type { CategoryData, ScriptData } from '@/application/collections/';
|
||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export type NodeDataErrorContext = {
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
} & (CategoryNodeErrorContext | ScriptNodeErrorContext | UnknownNodeErrorContext);
|
||||||
|
|
||||||
|
export type CategoryNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Category;
|
||||||
|
readonly selfNode: CategoryData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type ScriptNodeErrorContext = {
|
||||||
|
readonly type: NodeDataType.Script;
|
||||||
|
readonly selfNode: ScriptData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type UnknownNodeErrorContext = {
|
||||||
|
readonly type?: undefined;
|
||||||
|
readonly selfNode: NodeData;
|
||||||
|
readonly parentNode?: CategoryData;
|
||||||
|
};
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { NodeDataType } from './NodeDataType';
|
||||||
|
import type { NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeContextErrorMessageCreator {
|
||||||
|
(
|
||||||
|
errorMessage: string,
|
||||||
|
context: NodeDataErrorContext,
|
||||||
|
): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeContextErrorMessage: NodeContextErrorMessageCreator = (
|
||||||
|
errorMessage,
|
||||||
|
context,
|
||||||
|
) => {
|
||||||
|
let message = '';
|
||||||
|
if (context.type !== undefined) {
|
||||||
|
message += `${NodeDataType[context.type]}: `;
|
||||||
|
}
|
||||||
|
message += errorMessage;
|
||||||
|
message += `\n${getErrorContextDetails(context)}`;
|
||||||
|
return message;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getErrorContextDetails(context: NodeDataErrorContext): string {
|
||||||
|
let output = `Self: ${printNodeDataAsJson(context.selfNode)}`;
|
||||||
|
if (context.parentNode) {
|
||||||
|
output += `\nParent: ${printNodeDataAsJson(context.parentNode)}`;
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printNodeDataAsJson(node: NodeData): string {
|
||||||
|
return JSON.stringify(node, undefined, 2);
|
||||||
|
}
|
||||||
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
4
src/application/Parser/NodeValidation/NodeDataType.ts
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
export enum NodeDataType {
|
||||||
|
Script,
|
||||||
|
Category,
|
||||||
|
}
|
||||||
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
69
src/application/Parser/NodeValidation/NodeDataValidator.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { isString } from '@/TypeHelpers';
|
||||||
|
import { type NodeDataErrorContext } from './NodeDataErrorContext';
|
||||||
|
import { createNodeContextErrorMessage, type NodeContextErrorMessageCreator } from './NodeDataErrorContextMessage';
|
||||||
|
import type { NodeData } from './NodeData';
|
||||||
|
|
||||||
|
export interface NodeDataValidatorFactory {
|
||||||
|
(context: NodeDataErrorContext): NodeDataValidator;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NodeDataValidator {
|
||||||
|
assertValidName(nameValue: string): void;
|
||||||
|
assertDefined(
|
||||||
|
node: NodeData | undefined,
|
||||||
|
): asserts node is NonNullable<NodeData> & void;
|
||||||
|
assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true);
|
||||||
|
createContextualErrorMessage(errorMessage: string): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createNodeDataValidator
|
||||||
|
: NodeDataValidatorFactory = (context) => new ContextualNodeDataValidator(context);
|
||||||
|
|
||||||
|
export class ContextualNodeDataValidator implements NodeDataValidator {
|
||||||
|
constructor(
|
||||||
|
private readonly context: NodeDataErrorContext,
|
||||||
|
private readonly createErrorMessage
|
||||||
|
: NodeContextErrorMessageCreator = createNodeContextErrorMessage,
|
||||||
|
) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertValidName(nameValue: string): void {
|
||||||
|
this.assert(() => Boolean(nameValue), 'missing name');
|
||||||
|
this.assert(
|
||||||
|
() => isString(nameValue),
|
||||||
|
`Name (${JSON.stringify(nameValue)}) is not a string but ${typeof nameValue}.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assertDefined(
|
||||||
|
node: NodeData,
|
||||||
|
): asserts node is NonNullable<NodeData> {
|
||||||
|
this.assert(
|
||||||
|
() => node !== undefined && node !== null && Object.keys(node).length > 0,
|
||||||
|
'missing node data',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public assert(
|
||||||
|
validationPredicate: () => boolean,
|
||||||
|
errorMessage: string,
|
||||||
|
): asserts validationPredicate is (() => true) {
|
||||||
|
if (!validationPredicate()) {
|
||||||
|
this.throw(errorMessage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public createContextualErrorMessage(errorMessage: string): string {
|
||||||
|
return this.createErrorMessage(errorMessage, this.context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private throw(errorMessage: string): never {
|
||||||
|
throw new Error(
|
||||||
|
this.createContextualErrorMessage(errorMessage),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,10 +24,6 @@ parseProjectDetails(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ProjectDetailsParser {
|
|
||||||
(): ProjectDetails;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type ProjectDetailsFactory = (
|
export type ProjectDetailsFactory = (
|
||||||
...args: ConstructorArguments<typeof GitHubProjectDetails>
|
...args: ConstructorArguments<typeof GitHubProjectDetails>
|
||||||
) => ProjectDetails;
|
) => ProjectDetails;
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
import type { FunctionData } from '@/application/collections/';
|
||||||
|
import type { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||||
|
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||||
|
import { SyntaxFactory } from './Validation/Syntax/SyntaxFactory';
|
||||||
|
import type { ILanguageSyntax } from './Validation/Syntax/ILanguageSyntax';
|
||||||
|
import type { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||||
|
import type { ICategoryCollectionParseContext } from './ICategoryCollectionParseContext';
|
||||||
|
import type { ISyntaxFactory } from './Validation/Syntax/ISyntaxFactory';
|
||||||
|
|
||||||
|
export class CategoryCollectionParseContext implements ICategoryCollectionParseContext {
|
||||||
|
public readonly compiler: IScriptCompiler;
|
||||||
|
|
||||||
|
public readonly syntax: ILanguageSyntax;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
functionsData: ReadonlyArray<FunctionData> | undefined,
|
||||||
|
scripting: IScriptingDefinition,
|
||||||
|
syntaxFactory: ISyntaxFactory = new SyntaxFactory(),
|
||||||
|
) {
|
||||||
|
this.syntax = syntaxFactory.create(scripting.language);
|
||||||
|
this.compiler = new ScriptCompiler(functionsData ?? [], this.syntax);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgumentCollection } from '../../Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
import { ExpressionEvaluationContext, type IExpressionEvaluationContext } from './ExpressionEvaluationContext';
|
||||||
import { ExpressionPosition } from './ExpressionPosition';
|
import { ExpressionPosition } from './ExpressionPosition';
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext';
|
||||||
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
import { CompositeExpressionParser } from './Parser/CompositeExpressionParser';
|
||||||
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
import type { IReadOnlyFunctionCallArgumentCollection } from '../Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import type { IExpressionsCompiler } from './IExpressionsCompiler';
|
import type { IExpressionsCompiler } from './IExpressionsCompiler';
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
import { Expression, type ExpressionEvaluator } from '../../Expression/Expression';
|
||||||
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
import { createPositionFromRegexFullMatch, type ExpressionPositionFactory } from '../../Expression/ExpressionPositionFactory';
|
||||||
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from '../../../Function/Parameter/FunctionParameterCollectionFactory';
|
||||||
import type { IExpressionParser } from '../IExpressionParser';
|
import type { IExpressionParser } from '../IExpressionParser';
|
||||||
import type { IExpression } from '../../Expression/IExpression';
|
import type { IExpression } from '../../Expression/IExpression';
|
||||||
import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
|
import type { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter';
|
||||||
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
import type { IFunctionParameterCollection, IReadOnlyFunctionParameterCollection } from '../../../Function/Parameter/IFunctionParameterCollection';
|
||||||
|
|
||||||
export interface RegexParserUtilities {
|
export interface RegexParserUtilities {
|
||||||
@@ -110,7 +110,7 @@ function createParameters(
|
|||||||
|
|
||||||
export interface PrimitiveExpression {
|
export interface PrimitiveExpression {
|
||||||
readonly evaluator: ExpressionEvaluator;
|
readonly evaluator: ExpressionEvaluator;
|
||||||
readonly parameters?: readonly FunctionParameter[];
|
readonly parameters?: readonly IFunctionParameter[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ExpressionFactory {
|
export interface ExpressionFactory {
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
export interface Pipe {
|
export interface IPipe {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
apply(input: string): string;
|
apply(input: string): string;
|
||||||
}
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { Pipe } from '../Pipe';
|
import type { IPipe } from '../IPipe';
|
||||||
|
|
||||||
export class EscapeDoubleQuotes implements Pipe {
|
export class EscapeDoubleQuotes implements IPipe {
|
||||||
public readonly name: string = 'escapeDoubleQuotes';
|
public readonly name: string = 'escapeDoubleQuotes';
|
||||||
|
|
||||||
public apply(raw: string): string {
|
public apply(raw: string): string {
|
||||||
if (!raw) {
|
if (!raw) {
|
||||||
return '';
|
return raw;
|
||||||
}
|
}
|
||||||
return raw.replaceAll('"', '"^""');
|
return raw.replaceAll('"', '"^""');
|
||||||
/* eslint-disable vue/max-len */
|
/* eslint-disable vue/max-len */
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { splitTextIntoLines } from '@/application/Common/Text/SplitTextIntoLines';
|
import type { IPipe } from '../IPipe';
|
||||||
import type { Pipe } from '../Pipe';
|
|
||||||
|
|
||||||
export class InlinePowerShell implements Pipe {
|
export class InlinePowerShell implements IPipe {
|
||||||
public readonly name: string = 'inlinePowerShell';
|
public readonly name: string = 'inlinePowerShell';
|
||||||
|
|
||||||
public apply(code: string): string {
|
public apply(code: string): string {
|
||||||
@@ -9,11 +8,9 @@ export class InlinePowerShell implements Pipe {
|
|||||||
return code;
|
return code;
|
||||||
}
|
}
|
||||||
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
const processor = new Array<(data: string) => string>(...[ // for broken ESlint "indent"
|
||||||
// Order is important
|
|
||||||
inlineComments,
|
inlineComments,
|
||||||
mergeHereStrings,
|
|
||||||
mergeLinesWithBacktick,
|
mergeLinesWithBacktick,
|
||||||
mergeLinesWithBracketCodeBlocks,
|
mergeHereStrings,
|
||||||
mergeNewLines,
|
mergeNewLines,
|
||||||
]).reduce((a, b) => (data) => b(a(data)));
|
]).reduce((a, b) => (data) => b(a(data)));
|
||||||
const newCode = processor(code);
|
const newCode = processor(code);
|
||||||
@@ -92,6 +89,10 @@ function inlineComments(code: string): string {
|
|||||||
*/
|
*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLines(code: string): string[] {
|
||||||
|
return (code?.split(/\r\n|\r|\n/) || []);
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
Merges inline here-strings to a single lined string with Windows line terminator (\r\n)
|
||||||
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_quoting_rules?view=powershell-7.4#here-strings
|
||||||
@@ -101,18 +102,18 @@ function mergeHereStrings(code: string) {
|
|||||||
return code.replaceAll(regex, (_$, quotes, scope) => {
|
return code.replaceAll(regex, (_$, quotes, scope) => {
|
||||||
const newString = getHereStringHandler(quotes);
|
const newString = getHereStringHandler(quotes);
|
||||||
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
const escaped = scope.replaceAll(quotes, newString.escapedQuotes);
|
||||||
const lines = splitTextIntoLines(escaped);
|
const lines = getLines(escaped);
|
||||||
const inlined = lines.join(newString.separator);
|
const inlined = lines.join(newString.separator);
|
||||||
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
const quoted = `${newString.quotesAround}${inlined}${newString.quotesAround}`;
|
||||||
return quoted;
|
return quoted;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
interface InlinedHereString {
|
interface IInlinedHereString {
|
||||||
readonly quotesAround: string;
|
readonly quotesAround: string;
|
||||||
readonly escapedQuotes: string;
|
readonly escapedQuotes: string;
|
||||||
readonly separator: string;
|
readonly separator: string;
|
||||||
}
|
}
|
||||||
function getHereStringHandler(quotes: string): InlinedHereString {
|
function getHereStringHandler(quotes: string): IInlinedHereString {
|
||||||
/*
|
/*
|
||||||
We handle @' and @" differently.
|
We handle @' and @" differently.
|
||||||
Single quotes are interpreted literally and doubles are expandable.
|
Single quotes are interpreted literally and doubles are expandable.
|
||||||
@@ -157,33 +158,9 @@ function mergeLinesWithBacktick(code: string) {
|
|||||||
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
return code.replaceAll(/ +`\s*(?:\r\n|\r|\n)\s*/g, ' ');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Inlines code blocks in PowerShell scripts while preserving correct syntax.
|
|
||||||
* It removes unnecessary newlines and spaces around brackets,
|
|
||||||
* inlining the code where possible.
|
|
||||||
* This prevents syntax errors like "Unexpected token '}'" when inlining brackets.
|
|
||||||
*/
|
|
||||||
function mergeLinesWithBracketCodeBlocks(code: string): string {
|
|
||||||
return code
|
|
||||||
// Opening bracket: [whitespace] Opening bracket (newline)
|
|
||||||
.replace(/(?<=.*)\s*{[\r\n][\s\r\n]*/g, ' { ')
|
|
||||||
// Closing bracket: [whitespace] Closing bracket (newline) (continuation keyword)
|
|
||||||
.replace(/\s*}[\r\n][\s\r\n]*(?=elseif|else|catch|finally|until)/g, ' } ')
|
|
||||||
.replace(/(?<=do\s*{.*)[\r\n\s]*}[\r\n][\r\n\s]*(?=while)/g, ' } '); // Do-While
|
|
||||||
}
|
|
||||||
|
|
||||||
function mergeNewLines(code: string) {
|
function mergeNewLines(code: string) {
|
||||||
const nonEmptyLines = splitTextIntoLines(code)
|
return getLines(code)
|
||||||
.map((line) => line.trim())
|
.map((line) => line.trim())
|
||||||
.filter((line) => line.length > 0);
|
.filter((line) => line.length > 0)
|
||||||
|
.join('; ');
|
||||||
return nonEmptyLines
|
|
||||||
.map((line, index) => {
|
|
||||||
const isLastLine = index === nonEmptyLines.length - 1;
|
|
||||||
if (isLastLine) {
|
|
||||||
return line;
|
|
||||||
}
|
|
||||||
return line.endsWith(';') ? line : `${line};`;
|
|
||||||
})
|
|
||||||
.join(' ');
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
import { InlinePowerShell } from './PipeDefinitions/InlinePowerShell';
|
||||||
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
import { EscapeDoubleQuotes } from './PipeDefinitions/EscapeDoubleQuotes';
|
||||||
import type { Pipe } from './Pipe';
|
import type { IPipe } from './IPipe';
|
||||||
|
|
||||||
const RegisteredPipes = [
|
const RegisteredPipes = [
|
||||||
new EscapeDoubleQuotes(),
|
new EscapeDoubleQuotes(),
|
||||||
@@ -8,19 +8,19 @@ const RegisteredPipes = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
export interface IPipeFactory {
|
export interface IPipeFactory {
|
||||||
get(pipeName: string): Pipe;
|
get(pipeName: string): IPipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class PipeFactory implements IPipeFactory {
|
export class PipeFactory implements IPipeFactory {
|
||||||
private readonly pipes = new Map<string, Pipe>();
|
private readonly pipes = new Map<string, IPipe>();
|
||||||
|
|
||||||
constructor(pipes: readonly Pipe[] = RegisteredPipes) {
|
constructor(pipes: readonly IPipe[] = RegisteredPipes) {
|
||||||
for (const pipe of pipes) {
|
for (const pipe of pipes) {
|
||||||
this.registerPipe(pipe);
|
this.registerPipe(pipe);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public get(pipeName: string): Pipe {
|
public get(pipeName: string): IPipe {
|
||||||
validatePipeName(pipeName);
|
validatePipeName(pipeName);
|
||||||
const pipe = this.pipes.get(pipeName);
|
const pipe = this.pipes.get(pipeName);
|
||||||
if (!pipe) {
|
if (!pipe) {
|
||||||
@@ -29,7 +29,7 @@ export class PipeFactory implements IPipeFactory {
|
|||||||
return pipe;
|
return pipe;
|
||||||
}
|
}
|
||||||
|
|
||||||
private registerPipe(pipe: Pipe): void {
|
private registerPipe(pipe: IPipe): void {
|
||||||
validatePipeName(pipe.name);
|
validatePipeName(pipe.name);
|
||||||
if (this.pipes.has(pipe.name)) {
|
if (this.pipes.has(pipe.name)) {
|
||||||
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
throw new Error(`Pipe name must be unique: "${pipe.name}"`);
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
|
import { RegexParser, type PrimitiveExpression } from '../Parser/Regex/RegexParser';
|
||||||
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
import { ExpressionRegexBuilder } from '../Parser/Regex/ExpressionRegexBuilder';
|
||||||
|
|
||||||
@@ -15,10 +16,7 @@ export class ParameterSubstitutionParser extends RegexParser {
|
|||||||
const parameterName = match[1];
|
const parameterName = match[1];
|
||||||
const pipeline = match[2];
|
const pipeline = match[2];
|
||||||
return {
|
return {
|
||||||
parameters: [{
|
parameters: [new FunctionParameter(parameterName, false)],
|
||||||
name: parameterName,
|
|
||||||
isOptional: false,
|
|
||||||
}],
|
|
||||||
evaluator: (context) => {
|
evaluator: (context) => {
|
||||||
const { argumentValue } = context.args.getArgument(parameterName);
|
const { argumentValue } = context.args.getArgument(parameterName);
|
||||||
if (!pipeline) {
|
if (!pipeline) {
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
// eslint-disable-next-line max-classes-per-file
|
// eslint-disable-next-line max-classes-per-file
|
||||||
import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser';
|
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser';
|
||||||
import { FunctionParameterCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection';
|
||||||
|
import { FunctionParameter } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameter';
|
||||||
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';
|
import { createPositionFromRegexFullMatch } from '../Expression/ExpressionPositionFactory';
|
||||||
@@ -83,10 +84,7 @@ class WithStatementBuilder {
|
|||||||
|
|
||||||
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
|
public buildExpression(endExpressionPosition: ExpressionPosition, input: string): IExpression {
|
||||||
const parameters = new FunctionParameterCollection();
|
const parameters = new FunctionParameterCollection();
|
||||||
parameters.addParameter({
|
parameters.addParameter(new FunctionParameter(this.parameterName, true));
|
||||||
name: this.parameterName,
|
|
||||||
isOptional: true,
|
|
||||||
});
|
|
||||||
const position = new ExpressionPosition(
|
const position = new ExpressionPosition(
|
||||||
this.startExpressionPosition.start,
|
this.startExpressionPosition.start,
|
||||||
endExpressionPosition.end,
|
endExpressionPosition.end,
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { ensureValidParameterName } from '../../Shared/ParameterNameValidator';
|
||||||
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
|
export class FunctionCallArgument implements IFunctionCallArgument {
|
||||||
|
constructor(
|
||||||
|
public readonly parameterName: string,
|
||||||
|
public readonly argumentValue: string,
|
||||||
|
) {
|
||||||
|
ensureValidParameterName(parameterName);
|
||||||
|
if (!argumentValue) {
|
||||||
|
throw new Error(`Missing argument value for the parameter "${parameterName}".`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +1,10 @@
|
|||||||
import type { FunctionCallArgument } from './FunctionCallArgument';
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
import type { IFunctionCallArgumentCollection } from './IFunctionCallArgumentCollection';
|
||||||
|
|
||||||
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
export class FunctionCallArgumentCollection implements IFunctionCallArgumentCollection {
|
||||||
private readonly arguments = new Map<string, FunctionCallArgument>();
|
private readonly arguments = new Map<string, IFunctionCallArgument>();
|
||||||
|
|
||||||
public addArgument(argument: FunctionCallArgument): void {
|
public addArgument(argument: IFunctionCallArgument): void {
|
||||||
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`);
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@ export class FunctionCallArgumentCollection implements IFunctionCallArgumentColl
|
|||||||
return this.arguments.has(parameterName);
|
return this.arguments.has(parameterName);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getArgument(parameterName: string): FunctionCallArgument {
|
public getArgument(parameterName: string): IFunctionCallArgument {
|
||||||
if (!parameterName) {
|
if (!parameterName) {
|
||||||
throw new Error('missing parameter name');
|
throw new Error('missing parameter name');
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
export interface IFunctionCallArgument {
|
||||||
|
readonly parameterName: string;
|
||||||
|
readonly argumentValue: string;
|
||||||
|
}
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
import type { FunctionCallArgument } from './FunctionCallArgument';
|
import type { IFunctionCallArgument } from './IFunctionCallArgument';
|
||||||
|
|
||||||
export interface IReadOnlyFunctionCallArgumentCollection {
|
export interface IReadOnlyFunctionCallArgumentCollection {
|
||||||
getArgument(parameterName: string): FunctionCallArgument;
|
getArgument(parameterName: string): IFunctionCallArgument;
|
||||||
getAllParameterNames(): string[];
|
getAllParameterNames(): string[];
|
||||||
hasArgument(parameterName: string): boolean;
|
hasArgument(parameterName: string): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
|
||||||
addArgument(argument: FunctionCallArgument): void;
|
addArgument(argument: IFunctionCallArgument): void;
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
import { filterEmptyStrings } from '@/application/Common/Text/FilterEmptyStrings';
|
|
||||||
import type { CompiledCode } from '../CompiledCode';
|
import type { CompiledCode } from '../CompiledCode';
|
||||||
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
import type { CodeSegmentMerger } from './CodeSegmentMerger';
|
||||||
|
|
||||||
@@ -9,9 +8,11 @@ export class NewlineCodeSegmentMerger implements CodeSegmentMerger {
|
|||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
code: joinCodeParts(codeSegments.map((f) => f.code)),
|
||||||
revertCode: joinCodeParts(filterEmptyStrings(
|
revertCode: joinCodeParts(
|
||||||
codeSegments.map((f) => f.revertCode),
|
codeSegments
|
||||||
)),
|
.map((f) => f.revertCode)
|
||||||
|
.filter((code): code is string => Boolean(code)),
|
||||||
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import type { FunctionCall } from '../FunctionCall';
|
import type { FunctionCall } from '../FunctionCall';
|
||||||
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
import type { SingleCallCompiler } from './SingleCall/SingleCallCompiler';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection';
|
||||||
import type { CompiledCode } from './CompiledCode';
|
import type { CompiledCode } from './CompiledCode';
|
||||||
import type { FunctionCall } from '../FunctionCall';
|
import type { FunctionCall } from '../FunctionCall';
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
import { NewlineCodeSegmentMerger } from './CodeSegmentJoin/NewlineCodeSegmentMerger';
|
||||||
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
import { AdaptiveFunctionCallCompiler } from './SingleCall/AdaptiveFunctionCallCompiler';
|
||||||
import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
import type { ISharedFunctionCollection } from '../../ISharedFunctionCollection';
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
|
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction';
|
||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { CompiledCode } from '../CompiledCode';
|
import type { CompiledCode } from '../CompiledCode';
|
||||||
import type { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '../FunctionCallCompilationContext';
|
||||||
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
|
|
||||||
export interface ArgumentCompiler {
|
export interface ArgumentCompiler {
|
||||||
createCompiledNestedCall(
|
createCompiledNestedCall(
|
||||||
@@ -1,16 +1,20 @@
|
|||||||
import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
|
||||||
import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
||||||
import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler';
|
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
|
||||||
import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
|
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler';
|
||||||
import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
|
import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler';
|
||||||
import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall';
|
||||||
import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
|
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
|
||||||
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
|
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall';
|
||||||
import { createFunctionCallArgument, type FunctionCallArgument, type FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
|
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError';
|
||||||
import type { ArgumentCompiler } from './ArgumentCompiler';
|
import type { ArgumentCompiler } from './ArgumentCompiler';
|
||||||
|
|
||||||
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
||||||
constructor(private readonly utilities: ArgumentCompilationUtilities = DefaultUtilities) { }
|
constructor(
|
||||||
|
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
|
||||||
|
private readonly wrapError: ErrorWithContextWrapper
|
||||||
|
= wrapErrorWithAdditionalContext,
|
||||||
|
) { }
|
||||||
|
|
||||||
public createCompiledNestedCall(
|
public createCompiledNestedCall(
|
||||||
nestedFunction: FunctionCall,
|
nestedFunction: FunctionCall,
|
||||||
@@ -21,7 +25,10 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
|||||||
nestedFunction,
|
nestedFunction,
|
||||||
parentFunction.args,
|
parentFunction.args,
|
||||||
context,
|
context,
|
||||||
this.utilities,
|
{
|
||||||
|
expressionsCompiler: this.expressionsCompiler,
|
||||||
|
wrapError: this.wrapError,
|
||||||
|
},
|
||||||
);
|
);
|
||||||
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
const compiledCall = new ParsedFunctionCall(nestedFunction.functionName, compiledArgs);
|
||||||
return compiledCall;
|
return compiledCall;
|
||||||
@@ -31,7 +38,6 @@ export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
|
|||||||
interface ArgumentCompilationUtilities {
|
interface ArgumentCompilationUtilities {
|
||||||
readonly expressionsCompiler: IExpressionsCompiler,
|
readonly expressionsCompiler: IExpressionsCompiler,
|
||||||
readonly wrapError: ErrorWithContextWrapper;
|
readonly wrapError: ErrorWithContextWrapper;
|
||||||
readonly createCallArgument: FunctionCallArgumentFactory;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileNestedFunctionArguments(
|
function compileNestedFunctionArguments(
|
||||||
@@ -72,7 +78,7 @@ function compileNestedFunctionArguments(
|
|||||||
.map(({
|
.map(({
|
||||||
parameterName,
|
parameterName,
|
||||||
compiledArgumentValue,
|
compiledArgumentValue,
|
||||||
}) => utilities.createCallArgument(parameterName, compiledArgumentValue));
|
}) => new FunctionCallArgument(parameterName, compiledArgumentValue));
|
||||||
return buildArgumentCollectionFromArguments(compiledArguments);
|
return buildArgumentCollectionFromArguments(compiledArguments);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,9 +118,3 @@ function buildArgumentCollectionFromArguments(
|
|||||||
return compiledArgs;
|
return compiledArgs;
|
||||||
}, new FunctionCallArgumentCollection());
|
}, new FunctionCallArgumentCollection());
|
||||||
}
|
}
|
||||||
|
|
||||||
const DefaultUtilities: ArgumentCompilationUtilities = {
|
|
||||||
expressionsCompiler: new ExpressionsCompiler(),
|
|
||||||
wrapError: wrapErrorWithAdditionalContext,
|
|
||||||
createCallArgument: createFunctionCallArgument,
|
|
||||||
};
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user