Compare commits

...

21 Commits

Author SHA1 Message Date
undergroundwires
1430d5215a win: add more Edge scripts including AI & ads
This commit improves the scripts configuring Edge. It improves their
categorization, naming and adds scripts to disable Bing ads and Search
bar along with others to disable ads/data collection.

Changes:

- Add new scripts to configure Edge, such as blocking ads and AI
  features that collects data.
- Improve categorization and documentation consistency.
2024-06-26 16:48:49 +02:00
undergroundwires
c09c5ffa47 win, linux, mac: fix typos #373
This commit fixes typos, corrects markdown syntax, and archived URLs.

Co-authored-by: RainRat <rainrat78@yahoo.ca>
2024-06-26 08:13:13 +02:00
undergroundwires
ed7e69c07e win: add disabling Edge/WebView2 auto-updates #309
This commit adds scripts to block automatic updates for Microsoft Edge
and WebView2 on Windows, aimed at enhancing user privacy as per the
issue #309.

Changes:

- Create a new category for scripts targeting Edge and WebView2 updates.
- Add scripts for disabling automatic update services, scheduled tasks,
  and executable blocking, along with registry configurations.

Other supporting changes:

- Remove comments in code that indicates reusing of small text parts.
  This approach does not encourage creating unique content.
2024-06-25 12:23:55 +02:00
undergroundwires
f286f92b1f win: categorize, rename, doc Chrome & Edge scripts
This commit improves the script organization, documentation and code for
Edge and Chrome browser by simplifying naming, categorizing
configurations, and unifying documentation and generated code.

Changes:

- Rename "Edge (Chromium)" to "Edge" for clarity, with "Edge (Legacy)"
  detailed explicitly in the script titles.
- Flatten Edge settings under a unified "Configure" category.
- Enhance script documentation to improve clarity.
- Move "Your browser is managed" warning from script titles to script
  documentation.
- Introduce shared functions for configuring Edge and Chrome, leading to
  better consistency in generated code.
- Update scripts to include restart suggestions in generated code.
- Improve documentation of affected scripts.
- Split some scripts for increased granularity, easier maintenance and
  focused documentation.
- Fix some Windows UI scripts being incorrectly categorized as Edge
  configurations.
2024-06-24 20:27:52 +02:00
undergroundwires
e7031a3ae4 win: fix latest Edge removal on Windows 10 #309
This commit introduces a placeholder ifle creation step necessary for
the uninstallation process of Microsoft Edge on Windows 10, as discussed
in #309. The file simulates the presence of Microsoft Edge (Legacy),
which newer uninstallers check for before proceeding with the
uninstallation.

This change resolves the observed issue where the uninstaller fails to
recognize the absence of Legacy Edge, hindering the uninstallation
process.

Changes:

- Add placeholder file creation/removal for legacy Edge.
- Update and improve the documentation.
2024-06-23 12:51:15 +02:00
undergroundwires
2f828735a8 win: fix errors due to missing Edge uninstaller
If Edge is uninstalled using an existing installer, it may delete other
installers. When the script attempts to use these deleted installers,
it results in an error: `The system cannot find the file specified.`.

This commit addresses the issue by checking for the existence of the
uninstaller during the iteration and handling cases where it is missing.
2024-06-22 14:01:06 +02:00
undergroundwires
78c62cfc95 Trim compiler error output for better readability
Previously, compiler outputted whole executable in error context. This
caused long and hard to read error messages, especially when the
executable is a long category with many children. This commit improves
readability by trimming the error output.

Changes:

- Trim the error output (max characters: 1000).
- Improve indenting and newlines.
2024-06-21 12:46:30 +02:00
undergroundwires
ed93614ca3 Bump Electron to latest
- Bump Electron to latest.
- Adjust types to new Electron types.
2024-06-20 10:49:24 +02:00
undergroundwires
fac26a6ca0 Add type validation for parameters and fix types
This commit introduces type validation for parameter values within the
parser/compiler, aligning with the YAML schema. It aims to eliminate
dependencies on side effects in the collection files.

This update changes the treatment of data types in the Windows
collection, moving away from unintended type casting by the compiler.
Previously, numeric and boolean values were used even though only
string types were supported. This behavior was unstable and untested,
and has now been adjusted to use strings exclusively.

Changes ensure that parameter values are correctly validated
as strings, enhancing stability and maintainability.
2024-06-19 17:01:27 +02:00
undergroundwires
48761f62a2 win: fix incomplete VSCEIP, location scripts
This commit improves the validation logic in parser, corrects Windows
collection files to adhere to expected structure. This validation helps
catch errors that previously led to incomplete generated code in scripts
for disabling VSCEIP and location settings.

Changes:

- Add type validation for function call structures in the
  parser/compiler. This helps prevent runtime errors by ensuring that
  only correctly structured data is processed.
- Fix scripts in the Windows collection that previoulsy had incomplete
  `code` or `revertCode` values. These corrections ensure that the
  scripts function as intended.
- Refactor related logic within the compiler/parser to improve
  testability and maintainability.
2024-06-18 17:59:32 +02:00
undergroundwires
dc03bff324 Add schema validation for collection files #369
This commit improves collection file editing and error detection
directly in the IDE. It adds YAML schema, IDE configuration and
automatic tests to validate it.

- Introduce a YAML schema for collection file.
- Use `yaml-language-server` for enhanced YAML support in VSCode.
- Add telemetry disabling in `configure_vscode.py` to respect user
  privacy.
- Add automated checks to validate YAML file structure against the
  schema.
- Remove unused properties and do not allow them in compiler.
2024-06-17 14:01:07 +02:00
undergroundwires
e9a52859f6 mac: document, improve, encourage clearing logs
Previously, scripts under the 'Clear operating system logs' category for
macOS were misaligned due to a lack of individual script
recommendations, as the category itself wrongly used the `recommend:
strict` property. This misconfiguration caused none of these scripts to
appear recommended.

This commit assigns accurate `recommend:` values to each script
within the category.

Key changes:

- Introduce individual recommendations for each script.
- Document scripts to justify recommendations.
- Standardize deletion operations through shared functions.
- Improve script and category naming for clarity.
- Simplify code by unifying redundant path references.
- Add comments in generated user script code.
- Fix specific issue where clearing daily os logs inadvertently affected
  configuration files.
2024-06-16 11:27:48 +02:00
undergroundwires
1a10cf2e5f win: fix text and handwriting script omission #369
This commit corrects a syntax error that prevented the 'Disable text
and handwriting data collection' script from being included since
version 0.13.4. The error was identified in a previous syntax validation
update (commit 6ecfa9b954).

Changes:

- Add a missing dash before 'Disable location access' category. This
  fixes the script omission as reported in the issue #369.
- Remove the dash from `revertCode` of `HarvestContacts` registry
  modification code, fixing the revert code for 'Disable text and
  handwriting data collection' script.

This fix addresses the build errors introduced by stricter syntax checks
and ensures that the script is now properly recognized and executed.
This fix is part of ongoing efforts to improve data handling robustness
and management in script processing.
2024-06-15 08:32:58 +02:00
undergroundwires
1c2d82dc9b win: fix missing app access recommendations #369
This script fixes the recommendation property syntax in Windows script
collection. This syntax error prevented the application from
recommending these scripts, even though they were intended to be
recommended.

Affected scripts:

- Disable app access to physical movement
- Disable app access to eye tracking
- Disable app access to human presence
- Disable app access to screen capture

Previously, these scripts used the unsupported 'recommended: standard'
property, which was identified as incorrect after implementing stricter
property validation.
Related commit: 6ecfa9b954
Related issue: #369

This change update these properties to the correct 'recommend:
standard', resolving issues where scripts were not being recommended as
expected.
2024-06-14 12:36:15 +02:00
undergroundwires
6ecfa9b954 Add object property validation in parser #369
This commit introduces stricter type validation across the application
to reject objects with unexpected properties, enhancing the robustness
and predictability of data handling.

Changes include:

- Implement a common utility to validate object types.
- Refactor across various parsers and data handlers to utilize the new
  validations.
- Update error messages for better clarity and troubleshooting.
2024-06-13 22:26:57 +02:00
undergroundwires
c138f74460 Refactor to unify scripts/categories as Executable
This commit consolidates scripts and categories under a unified
'Executable' concept. This simplifies the architecture and improves code
readability.

- Introduce subfolders within `src/domain` to segregate domain elements.
- Update class and interface names by removing the 'I' prefix in
  alignment with new coding standards.
- Replace 'Node' with 'Executable' to clarify usage; reserve 'Node'
  exclusively for the UI's tree component.
2024-06-12 12:36:40 +02:00
undergroundwires
8becc7dbc4 win: fix revert scripts for removing shortcuts
Revert scripts for removing shortcuts previously used hardcoded paths.
These paths are now replaced with system environment variables to allow
for broader configuration compatibility.
2024-06-11 12:06:46 +02:00
undergroundwires
b29cd7b5f7 mac: discourage and document captive portal script
This commit adjusts the recommendation level for disabling captive
portal detection from 'Standard' to 'Strict'. This aligns macOS settings
with equivalent recommendations for Linux and Windows.

It improves documentation to provide additional context on implications,
facilitating a better understanding of the change.
2024-06-10 13:22:32 +02:00
undergroundwires
f21ef9250a win: improve executable blocking, Chrome reporting
This commit improves blocking of execution of executables, providing a
more reliable way to stop execution of unwanted executables.

Introduce a new function to block shell execution of an executable. This
logic is extracted from disabling Chrome Software Reporter tool with
improved logic which does no longer or reset if there is other rules.
This resolves potential issues if there was a blocking rule using same
number which privacy.sexy before overwrote or restored on revert.

Other scripts which terminated executables on launch does now block
their shell execution too for more reliability. A common function is
introduced which streamlines blocking execution of an executable all
known ways which is now reused by these scripts.

This commit additionally improves the Google Software Reporter disabling
script. It removes the code that adds denies permissions on its
installation directory as the new way of preventing executable from
running should be enough. It also adds missing documentation to the
related scripts.
2024-06-09 13:59:06 +02:00
undergroundwires
fa2a92bf89 Add image to README.md to thank supporters 2024-06-08 11:49:04 +02:00
undergroundwires
8341411be4 win: document and improve Firefox telemetry #259
This commit improves the existing Firefox privacy scripts and improves
the categorization and documentation to be simpler and more clear.

Changes:

- Rename and reorganize scripts for disabling browser telemetry and
  default browser agent reporting to simplify the structure.
- Improve documentation across scripts to provide clearer guidance on
  how the changes improve user privacy.
- Fix revert scripts by removing unnecessary registry key configuration.
2024-05-29 08:17:50 +02:00
290 changed files with 8759 additions and 3333 deletions

View File

@@ -74,3 +74,28 @@ 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
View File

@@ -14,3 +14,7 @@ node_modules
# macOS # macOS
.DS_Store .DS_Store
# Python
__pycache__
.venv

View File

@@ -5,8 +5,10 @@
"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.
// Documentation // Markdown
"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.

View File

@@ -186,3 +186,7 @@ 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
[![Supporters appreciation banner showing the supporters](https://undergroundwires.dev/img/supporters.jpg)](https://undergroundwires.dev/supporters)

View File

@@ -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/Script/Compiler/Expressions/SyntaxParsers) where you can look at other parsers to understand more. 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.
2. Register your in [CompositeExpressionParser](./../src/application/Parser/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts). 2. Register your in [CompositeExpressionParser](./../src/application/Parser/Executable/Script/Compiler/Expressions/Parser/CompositeExpressionParser.ts).

View File

@@ -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. 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.
Related documentation: Related documentation:
- 📖 [`collection.yaml.d.ts`](./../src/application/collections/collection.yaml.d.ts) outlines code types. - 📖 [`Collections README`](./../src/application/collections/README.md) includes references to code as documentation.
- 📖 [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,11 +28,20 @@ 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.
### `Category` ### Executables
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.
@@ -43,7 +52,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.
@@ -58,7 +67,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.

View File

@@ -80,8 +80,10 @@ 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.
- [**`python ./scripts/configure_vscode.py`**](../scripts/configure_vscode.py): - [**`python3 ./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

17
package-lock.json generated
View File

@@ -6,7 +6,7 @@
"packages": { "packages": {
"": { "": {
"name": "privacy.sexy", "name": "privacy.sexy",
"version": "0.13.3", "version": "0.13.4",
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@floating-ui/vue": "^1.0.6", "@floating-ui/vue": "^1.0.6",
@@ -34,7 +34,7 @@
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.7.3", "cypress": "^13.7.3",
"electron": "^29.3.0", "electron": "^31.0.2",
"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.1.0", "electron-vite": "^2.1.0",
@@ -6645,11 +6645,12 @@
} }
}, },
"node_modules/electron": { "node_modules/electron": {
"version": "29.3.0", "version": "31.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-31.0.2.tgz",
"integrity": "sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==", "integrity": "sha512-55efQ5yfLN+AQHcFC00AXQqtxC3iAGaxX2GQ3EDbFJ0ca9GHNOdSXkcrdBElLleiDrR2hpXNkQxN1bDn0oxe6w==",
"dev": true, "dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",
"@types/node": "^20.9.0", "@types/node": "^20.9.0",
@@ -22338,9 +22339,9 @@
} }
}, },
"electron": { "electron": {
"version": "29.3.0", "version": "31.0.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-29.3.0.tgz", "resolved": "https://registry.npmjs.org/electron/-/electron-31.0.2.tgz",
"integrity": "sha512-ZxFKm0/v48GSoBuO3DdnMlCYXefEUKUHLMsKxyXY4nZGgzbBKpF/X8haZa2paNj23CLfsCKBOtfc2vsEQiOOsA==", "integrity": "sha512-55efQ5yfLN+AQHcFC00AXQqtxC3iAGaxX2GQ3EDbFJ0ca9GHNOdSXkcrdBElLleiDrR2hpXNkQxN1bDn0oxe6w==",
"dev": true, "dev": true,
"requires": { "requires": {
"@electron/get": "^2.0.0", "@electron/get": "^2.0.0",

View File

@@ -59,7 +59,7 @@
"@vue/test-utils": "^2.4.5", "@vue/test-utils": "^2.4.5",
"autoprefixer": "^10.4.19", "autoprefixer": "^10.4.19",
"cypress": "^13.7.3", "cypress": "^13.7.3",
"electron": "^29.3.0", "electron": "^31.0.2",
"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.1.0", "electron-vite": "^2.1.0",

View File

@@ -58,6 +58,10 @@ 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:

View File

@@ -0,0 +1,51 @@
# 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
```

View File

@@ -0,0 +1,62 @@
"""
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()

View File

@@ -0,0 +1,6 @@
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

View File

@@ -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 IEnumParser<TEnum> { export interface EnumParser<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>,
): IEnumParser<TEnumValue> { ): EnumParser<TEnumValue> {
return { return {
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable), parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
}; };

View File

@@ -1,4 +1,4 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
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 type { ICodeChangedEvent } from './ICodeChangedEvent'; import type { ICodeChangedEvent } from './ICodeChangedEvent';
@@ -6,13 +6,13 @@ 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<IScript>; public readonly addedScripts: ReadonlyArray<Script>;
public readonly removedScripts: ReadonlyArray<IScript>; public readonly removedScripts: ReadonlyArray<Script>;
public readonly changedScripts: ReadonlyArray<IScript>; public readonly changedScripts: ReadonlyArray<Script>;
private readonly scripts: Map<IScript, ICodePosition>; private readonly scripts: Map<Script, ICodePosition>;
constructor( constructor(
code: string, code: string,
@@ -25,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<IScript, ICodePosition>(); this.scripts = new Map<Script, ICodePosition>();
scripts.forEach((position, selection) => { scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position); this.scripts.set(selection.script, position);
}); });
@@ -35,7 +35,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
return this.scripts.size === 0; return this.scripts.size === 0;
} }
public getScriptPositionInCode(script: IScript): ICodePosition { public getScriptPositionInCode(script: Script): ICodePosition {
return this.getPositionById(script.id); return this.getPositionById(script.id);
} }
@@ -65,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<IScript> { ): ReadonlyArray<Script> {
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))

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
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<IScript>; readonly addedScripts: ReadonlyArray<Script>;
readonly removedScripts: ReadonlyArray<IScript>; readonly removedScripts: ReadonlyArray<Script>;
readonly changedScripts: ReadonlyArray<IScript>; readonly changedScripts: ReadonlyArray<Script>;
isEmpty(): boolean; isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition; getScriptPositionInCode(script: Script): ICodePosition;
} }

View File

@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
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<IScript>, public readonly scriptMatches: ReadonlyArray<Script>,
public readonly categoryMatches: ReadonlyArray<ICategory>, public readonly categoryMatches: ReadonlyArray<Category>,
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'); }

View File

@@ -1,8 +1,9 @@
import type { IScript, ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { Script } from '@/domain/Executables/Script/Script';
export interface FilterResult { export interface FilterResult {
readonly categoryMatches: ReadonlyArray<ICategory>; readonly categoryMatches: ReadonlyArray<Category>;
readonly scriptMatches: ReadonlyArray<IScript>; readonly scriptMatches: ReadonlyArray<Script>;
readonly query: string; readonly query: string;
hasAnyMatches(): boolean; hasAnyMatches(): boolean;
} }

View File

@@ -1,7 +1,8 @@
import type { ICategory, IScript } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { IScriptCode } from '@/domain/IScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
import type { IDocumentable } from '@/domain/IDocumentable'; import type { Documentable } from '@/domain/Executables/Documentable';
import type { ICategoryCollection } from '@/domain/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';
@@ -24,7 +25,7 @@ export class LinearFilterStrategy implements FilterStrategy {
} }
function matchesCategory( function matchesCategory(
category: ICategory, category: Category,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -34,7 +35,7 @@ function matchesCategory(
} }
function matchesScript( function matchesScript(
script: IScript, script: Script,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return matchesAny( return matchesAny(
@@ -58,7 +59,7 @@ function matchName(
} }
function matchCode( function matchCode(
code: IScriptCode, code: ScriptCode,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
if (code.execute.toLowerCase().includes(filterLowercase)) { if (code.execute.toLowerCase().includes(filterLowercase)) {
@@ -71,7 +72,7 @@ function matchCode(
} }
function matchDocumentation( function matchDocumentation(
documentable: IDocumentable, documentable: Documentable,
filterLowercase: string, filterLowercase: string,
): boolean { ): boolean {
return documentable.docs.some( return documentable.docs.some(

View File

@@ -1,9 +1,9 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { CategorySelectionChangeCommand } from './CategorySelectionChange'; import type { CategorySelectionChangeCommand } from './CategorySelectionChange';
export interface ReadonlyCategorySelection { export interface ReadonlyCategorySelection {
areAllScriptsSelected(category: ICategory): boolean; areAllScriptsSelected(category: Category): boolean;
isAnyScriptSelected(category: ICategory): boolean; isAnyScriptSelected(category: Category): boolean;
} }
export interface CategorySelection extends ReadonlyCategorySelection { export interface CategorySelection extends ReadonlyCategorySelection {

View File

@@ -1,4 +1,4 @@
import type { ICategory } from '@/domain/ICategory'; import type { Category } from '@/domain/Executables/Category/Category';
import type { ICategoryCollection } from '@/domain/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';
@@ -13,7 +13,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
} }
public areAllScriptsSelected(category: ICategory): boolean { public areAllScriptsSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;
@@ -27,7 +27,7 @@ export class ScriptToCategorySelectionMapper implements CategorySelection {
); );
} }
public isAnyScriptSelected(category: ICategory): boolean { public isAnyScriptSelected(category: Category): boolean {
const { selectedScripts } = this.scriptSelection; const { selectedScripts } = this.scriptSelection;
if (selectedScripts.length === 0) { if (selectedScripts.length === 0) {
return false; return false;

View File

@@ -1,5 +1,5 @@
import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository'; import { InMemoryRepository } from '@/infrastructure/Repository/InMemoryRepository';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
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/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
@@ -80,7 +80,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
}); });
} }
public selectOnly(scripts: readonly IScript[]): void { public selectOnly(scripts: readonly Script[]): void {
assertNonEmptyScriptSelection(scripts); assertNonEmptyScriptSelection(scripts);
this.processChanges({ this.processChanges({
changes: [ changes: [
@@ -145,7 +145,7 @@ export class DebouncedScriptSelection implements ScriptSelection {
} }
} }
function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) { function assertNonEmptyScriptSelection(selectedItems: readonly Script[]) {
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.');
} }
@@ -153,7 +153,7 @@ function assertNonEmptyScriptSelection(selectedItems: readonly IScript[]) {
function getScriptIdsToBeSelected( function getScriptIdsToBeSelected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return desiredScripts return desiredScripts
.filter((script) => !existingItems.exists(script.id)) .filter((script) => !existingItems.exists(script.id))
@@ -162,7 +162,7 @@ function getScriptIdsToBeSelected(
function getScriptIdsToBeDeselected( function getScriptIdsToBeDeselected(
existingItems: ReadonlyRepository<string, SelectedScript>, existingItems: ReadonlyRepository<string, SelectedScript>,
desiredScripts: readonly IScript[], desiredScripts: readonly Script[],
): string[] { ): string[] {
return existingItems return existingItems
.getItems() .getItems()

View File

@@ -1,5 +1,5 @@
import type { IEventSource } from '@/infrastructure/Events/IEventSource'; import type { IEventSource } from '@/infrastructure/Events/IEventSource';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange'; import type { ScriptSelectionChangeCommand } from './ScriptSelectionChange';
@@ -10,7 +10,7 @@ export interface ReadonlyScriptSelection {
} }
export interface ScriptSelection extends ReadonlyScriptSelection { export interface ScriptSelection extends ReadonlyScriptSelection {
selectOnly(scripts: readonly IScript[]): void; selectOnly(scripts: readonly Script[]): void;
selectAll(): void; selectAll(): void;
deselectAll(): void; deselectAll(): void;
processChanges(action: ScriptSelectionChangeCommand): void; processChanges(action: ScriptSelectionChangeCommand): void;

View File

@@ -1,9 +1,9 @@
import type { IEntity } from '@/infrastructure/Entity/IEntity'; import type { IEntity } from '@/infrastructure/Entity/IEntity';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
type ScriptId = IScript['id']; type ScriptId = Script['id'];
export interface SelectedScript extends IEntity<ScriptId> { export interface SelectedScript extends IEntity<ScriptId> {
readonly script: IScript; readonly script: Script;
readonly revert: boolean; readonly revert: boolean;
} }

View File

@@ -1,12 +1,12 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity'; import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import type { IScript } from '@/domain/IScript'; import type { Script } from '@/domain/Executables/Script/Script';
import type { SelectedScript } from './SelectedScript'; import type { SelectedScript } from './SelectedScript';
type SelectedScriptId = SelectedScript['id']; type SelectedScriptId = SelectedScript['id'];
export class UserSelectedScript extends BaseEntity<SelectedScriptId> { export class UserSelectedScript extends BaseEntity<SelectedScriptId> {
constructor( constructor(
public readonly script: IScript, public readonly script: Script,
public readonly revert: boolean, public readonly revert: boolean,
) { ) {
super(script.id); super(script.id);

View File

@@ -1,40 +1,48 @@
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 } from '@/application/Parser/ProjectDetailsParser'; import { parseProjectDetails, type ProjectDetailsParser } from '@/application/Parser/ProjectDetailsParser';
import { Application } from '@/domain/Application'; import { Application } from '@/domain/Application';
import type { IAppMetadata } from '@/infrastructure/EnvironmentVariables/IAppMetadata'; import { parseCategoryCollection, type CategoryCollectionParser } from './CategoryCollectionParser';
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { parseCategoryCollection } from './CategoryCollectionParser';
export function parseApplication( export function parseApplication(
categoryParser = parseCategoryCollection, collectionsData: readonly CollectionData[] = PreParsedCollections,
projectDetailsParser = parseProjectDetails, utilities: ApplicationParserUtilities = DefaultUtilities,
metadata: IAppMetadata = EnvironmentVariablesFactory.Current.instance,
collectionsData = PreParsedCollections,
): IApplication { ): IApplication {
validateCollectionsData(collectionsData); validateCollectionsData(collectionsData, utilities.validator);
const projectDetails = projectDetailsParser(metadata); const projectDetails = utilities.parseProjectDetails();
const collections = collectionsData.map( const collections = collectionsData.map(
(collection) => categoryParser(collection, projectDetails), (collection) => utilities.parseCategoryCollection(collection, projectDetails),
); );
const app = new Application(projectDetails, collections); const app = new Application(projectDetails, collections);
return app; return app;
} }
export type CategoryCollectionParserType const PreParsedCollections: readonly CollectionData[] = [
= (file: CollectionData, projectDetails: ProjectDetails) => ICategoryCollection;
const PreParsedCollections: readonly CollectionData [] = [
WindowsData, MacOsData, LinuxData, WindowsData, MacOsData, LinuxData,
]; ];
function validateCollectionsData(collections: readonly CollectionData[]) { function validateCollectionsData(
if (!collections.length) { collections: readonly CollectionData[],
throw new Error('missing collections'); validator: TypeValidator,
} ) {
validator.assertNonEmptyCollection({
value: collections,
valueName: 'collections',
});
} }
interface ApplicationParserUtilities {
readonly parseCategoryCollection: CategoryCollectionParser;
readonly validator: TypeValidator;
readonly parseProjectDetails: ProjectDetailsParser;
}
const DefaultUtilities: ApplicationParserUtilities = {
parseCategoryCollection,
parseProjectDetails,
validator: createTypeValidator(),
};

View File

@@ -3,32 +3,73 @@ import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection'; import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import { CategoryCollection } from '@/domain/CategoryCollection'; import { CategoryCollection } from '@/domain/CategoryCollection';
import type { ProjectDetails } from '@/domain/Project/ProjectDetails'; import type { ProjectDetails } from '@/domain/Project/ProjectDetails';
import { createEnumParser } from '../Common/Enum'; import { createEnumParser, type EnumParser } from '../Common/Enum';
import { parseCategory } from './CategoryParser'; import { parseCategory, type CategoryParser } from './Executable/CategoryParser';
import { CategoryCollectionParseContext } from './Script/CategoryCollectionParseContext'; import { parseScriptingDefinition, type ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser';
import { ScriptingDefinitionParser } from './ScriptingDefinition/ScriptingDefinitionParser'; import { createTypeValidator, type TypeValidator } from './Common/TypeValidator';
import { createCollectionUtilities, type CategoryCollectionSpecificUtilitiesFactory } from './Executable/CategoryCollectionSpecificUtilities';
export function parseCategoryCollection( export const parseCategoryCollection: CategoryCollectionParser = (
content: CollectionData, content,
projectDetails: ProjectDetails, projectDetails,
osParser = createEnumParser(OperatingSystem), utilities: CategoryCollectionParserUtilities = DefaultUtilities,
): ICategoryCollection { ) => {
validate(content); validateCollection(content, utilities.validator);
const scripting = new ScriptingDefinitionParser() const scripting = utilities.parseScriptingDefinition(content.scripting, projectDetails);
.parse(content.scripting, projectDetails); const collectionUtilities = utilities.createUtilities(content.functions, scripting);
const context = new CategoryCollectionParseContext(content.functions, scripting); const categories = content.actions.map(
const categories = content.actions.map((action) => parseCategory(action, context)); (action) => utilities.parseCategory(action, collectionUtilities),
const os = osParser.parseEnum(content.os, 'os');
const collection = new CategoryCollection(
os,
categories,
scripting,
); );
const os = utilities.osParser.parseEnum(content.os, 'os');
const collection = utilities.createCategoryCollection({
os, actions: categories, scripting,
});
return collection; return collection;
};
export type CategoryCollectionFactory = (
...parameters: ConstructorParameters<typeof CategoryCollection>
) => ICategoryCollection;
export interface CategoryCollectionParser {
(
content: CollectionData,
projectDetails: ProjectDetails,
utilities?: CategoryCollectionParserUtilities,
): ICategoryCollection;
} }
function validate(content: CollectionData): void { function validateCollection(
if (!content.actions.length) { content: CollectionData,
throw new Error('content does not define any action'); validator: TypeValidator,
} ): void {
validator.assertObject({
value: content,
valueName: 'collection',
allowedProperties: [
'os', 'scripting', 'actions', 'functions',
],
});
validator.assertNonEmptyCollection({
value: content.actions,
valueName: '"actions" in collection',
});
} }
interface CategoryCollectionParserUtilities {
readonly osParser: EnumParser<OperatingSystem>;
readonly validator: TypeValidator;
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),
};

View File

@@ -1,171 +0,0 @@
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,
};

View File

@@ -0,0 +1,131 @@
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.`);
}
}

View File

@@ -0,0 +1,35 @@
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;
}

View File

@@ -0,0 +1,187 @@
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 { CollectionCategory } from '@/domain/Executables/Category/CollectionCategory';
import type { Script } from '@/domain/Executables/Script/Script';
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';
let categoryIdCounter = 0;
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({
id: categoryIdCounter++,
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',
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);
}
export type CategoryFactory = (
...parameters: ConstructorParameters<typeof CollectionCategory>
) => Category;
interface CategoryParserUtilities {
readonly createCategory: CategoryFactory;
readonly wrapError: ErrorWithContextWrapper;
readonly createValidator: ExecutableValidatorFactory;
readonly parseScript: ScriptParser;
readonly parseDocs: DocsParser;
}
const DefaultCategoryParserUtilities: CategoryParserUtilities = {
createCategory: (...parameters) => new CollectionCategory(...parameters),
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
parseScript,
parseDocs,
};

View File

@@ -50,5 +50,5 @@ class DocumentationContainer {
} }
function throwInvalidType(): never { function throwInvalidType(): never {
throw new Error('docs field (documentation) must be an array of strings'); throw new Error('docs field (documentation) must be a single string or an array of strings.');
} }

View File

@@ -1,4 +1,4 @@
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Executable/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';

View File

@@ -1,4 +1,4 @@
import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Script/Compiler/Expressions/Expression/ExpressionEvaluationContext'; import { type IExpressionEvaluationContext, ExpressionEvaluationContext } from '@/application/Parser/Executable/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';

View File

@@ -1,10 +1,10 @@
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/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 { IFunctionParameter } from '../../../Function/Parameter/IFunctionParameter'; import type { FunctionParameter } from '../../../Function/Parameter/FunctionParameter';
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 IFunctionParameter[]; readonly parameters?: readonly FunctionParameter[];
} }
export interface ExpressionFactory { export interface ExpressionFactory {

View File

@@ -1,4 +1,3 @@
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';
@@ -16,7 +15,10 @@ export class ParameterSubstitutionParser extends RegexParser {
const parameterName = match[1]; const parameterName = match[1];
const pipeline = match[2]; const pipeline = match[2];
return { return {
parameters: [new FunctionParameter(parameterName, false)], parameters: [{
name: parameterName,
isOptional: false,
}],
evaluator: (context) => { evaluator: (context) => {
const { argumentValue } = context.args.getArgument(parameterName); const { argumentValue } = context.args.getArgument(parameterName);
if (!pipeline) { if (!pipeline) {

View File

@@ -1,7 +1,6 @@
// eslint-disable-next-line max-classes-per-file // eslint-disable-next-line max-classes-per-file
import type { IExpressionParser } from '@/application/Parser/Script/Compiler/Expressions/Parser/IExpressionParser'; import type { IExpressionParser } from '@/application/Parser/Executable/Script/Compiler/Expressions/Parser/IExpressionParser';
import { FunctionParameterCollection } from '@/application/Parser/Script/Compiler/Function/Parameter/FunctionParameterCollection'; import { FunctionParameterCollection } from '@/application/Parser/Executable/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';
@@ -84,7 +83,10 @@ 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(new FunctionParameter(this.parameterName, true)); parameters.addParameter({
name: this.parameterName,
isOptional: true,
});
const position = new ExpressionPosition( const position = new ExpressionPosition(
this.startExpressionPosition.start, this.startExpressionPosition.start,
endExpressionPosition.end, endExpressionPosition.end,

View File

@@ -0,0 +1,41 @@
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: `Missing argument value for the parameter "${parameterName}".`,
});
return {
parameterName,
argumentValue,
};
};
interface FunctionCallArgumentFactoryUtilities {
readonly typeValidator: TypeValidator;
readonly validateParameterName: ParameterNameValidator;
}
const DefaultUtilities: FunctionCallArgumentFactoryUtilities = {
typeValidator: createTypeValidator(),
validateParameterName,
};

View File

@@ -1,10 +1,10 @@
import type { IFunctionCallArgument } from './IFunctionCallArgument'; import type { FunctionCallArgument } from './FunctionCallArgument';
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, IFunctionCallArgument>(); private readonly arguments = new Map<string, FunctionCallArgument>();
public addArgument(argument: IFunctionCallArgument): void { public addArgument(argument: FunctionCallArgument): 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): IFunctionCallArgument { public getArgument(parameterName: string): FunctionCallArgument {
if (!parameterName) { if (!parameterName) {
throw new Error('missing parameter name'); throw new Error('missing parameter name');
} }

View File

@@ -1,11 +1,11 @@
import type { IFunctionCallArgument } from './IFunctionCallArgument'; import type { FunctionCallArgument } from './FunctionCallArgument';
export interface IReadOnlyFunctionCallArgumentCollection { export interface IReadOnlyFunctionCallArgumentCollection {
getArgument(parameterName: string): IFunctionCallArgument; getArgument(parameterName: string): FunctionCallArgument;
getAllParameterNames(): string[]; getAllParameterNames(): string[];
hasArgument(parameterName: string): boolean; hasArgument(parameterName: string): boolean;
} }
export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection { export interface IFunctionCallArgumentCollection extends IReadOnlyFunctionCallArgumentCollection {
addArgument(argument: IFunctionCallArgument): void; addArgument(argument: FunctionCallArgument): void;
} }

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Executable/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';

View File

@@ -1,4 +1,4 @@
import type { ISharedFunctionCollection } from '@/application/Parser/Script/Compiler/Function/ISharedFunctionCollection'; import type { ISharedFunctionCollection } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunctionCollection';
import type { CompiledCode } from './CompiledCode'; import type { CompiledCode } from './CompiledCode';
import type { FunctionCall } from '../FunctionCall'; import type { FunctionCall } from '../FunctionCall';

View File

@@ -1,4 +1,4 @@
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/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';

View File

@@ -1,5 +1,5 @@
import type { ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import type { ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/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';

View File

@@ -1,5 +1,5 @@
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
export interface ArgumentCompiler { export interface ArgumentCompiler {
createCompiledNestedCall( createCompiledNestedCall(

View File

@@ -1,20 +1,16 @@
import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection'; import type { IReadOnlyFunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/IFunctionCallArgumentCollection';
import { FunctionCallArgument } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgument'; import { FunctionCallArgumentCollection } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection';
import { FunctionCallArgumentCollection } from '@/application/Parser/Script/Compiler/Function/Call/Argument/FunctionCallArgumentCollection'; import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler';
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; 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'; import { ParsedFunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/ParsedFunctionCall';
import { ParsedFunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/ParsedFunctionCall'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError'; import { createFunctionCallArgument, type FunctionCallArgument, type FunctionCallArgumentFactory } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Argument/FunctionCallArgument';
import type { ArgumentCompiler } from './ArgumentCompiler'; import type { ArgumentCompiler } from './ArgumentCompiler';
export class NestedFunctionArgumentCompiler implements ArgumentCompiler { export class NestedFunctionArgumentCompiler implements ArgumentCompiler {
constructor( constructor(private readonly utilities: ArgumentCompilationUtilities = DefaultUtilities) { }
private readonly expressionsCompiler: IExpressionsCompiler = new ExpressionsCompiler(),
private readonly wrapError: ErrorWithContextWrapper
= wrapErrorWithAdditionalContext,
) { }
public createCompiledNestedCall( public createCompiledNestedCall(
nestedFunction: FunctionCall, nestedFunction: FunctionCall,
@@ -25,10 +21,7 @@ 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;
@@ -38,6 +31,7 @@ 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(
@@ -78,7 +72,7 @@ function compileNestedFunctionArguments(
.map(({ .map(({
parameterName, parameterName,
compiledArgumentValue, compiledArgumentValue,
}) => new FunctionCallArgument(parameterName, compiledArgumentValue)); }) => utilities.createCallArgument(parameterName, compiledArgumentValue));
return buildArgumentCollectionFromArguments(compiledArguments); return buildArgumentCollectionFromArguments(compiledArguments);
} }
@@ -118,3 +112,9 @@ function buildArgumentCollectionFromArguments(
return compiledArgs; return compiledArgs;
}, new FunctionCallArgumentCollection()); }, new FunctionCallArgumentCollection());
} }
const DefaultUtilities: ArgumentCompilationUtilities = {
expressionsCompiler: new ExpressionsCompiler(),
wrapError: wrapErrorWithAdditionalContext,
createCallArgument: createFunctionCallArgument,
};

View File

@@ -1,8 +1,8 @@
import { ExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/ExpressionsCompiler'; import { ExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/ExpressionsCompiler';
import type { IExpressionsCompiler } from '@/application/Parser/Script/Compiler/Expressions/IExpressionsCompiler'; import type { IExpressionsCompiler } from '@/application/Parser/Executable/Script/Compiler/Expressions/IExpressionsCompiler';
import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; import { FunctionBodyType, type ISharedFunction } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy { export class InlineFunctionCallCompiler implements SingleCallCompilerStrategy {

View File

@@ -1,11 +1,11 @@
import { import {
type CallFunctionBody, FunctionBodyType, type CallFunctionBody, FunctionBodyType,
type ISharedFunction, type ISharedFunction,
} from '@/application/Parser/Script/Compiler/Function/ISharedFunction'; } from '@/application/Parser/Executable/Script/Compiler/Function/ISharedFunction';
import type { FunctionCall } from '@/application/Parser/Script/Compiler/Function/Call/FunctionCall'; import type { FunctionCall } from '@/application/Parser/Executable/Script/Compiler/Function/Call/FunctionCall';
import type { FunctionCallCompilationContext } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext'; import type { FunctionCallCompilationContext } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/FunctionCallCompilationContext';
import type { CompiledCode } from '@/application/Parser/Script/Compiler/Function/Call/Compiler/CompiledCode'; import type { CompiledCode } from '@/application/Parser/Executable/Script/Compiler/Function/Call/Compiler/CompiledCode';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler'; import { NestedFunctionArgumentCompiler } from './Argument/NestedFunctionArgumentCompiler';
import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy'; import type { SingleCallCompilerStrategy } from '../SingleCallCompilerStrategy';
import type { ArgumentCompiler } from './Argument/ArgumentCompiler'; import type { ArgumentCompiler } from './Argument/ArgumentCompiler';

View File

@@ -0,0 +1,80 @@
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());
}

View File

@@ -1,4 +1,4 @@
export interface IFunctionParameter { export interface FunctionParameter {
readonly name: string; readonly name: string;
readonly isOptional: boolean; readonly isOptional: boolean;
} }

View File

@@ -1,14 +1,14 @@
import type { IFunctionParameterCollection } from './IFunctionParameterCollection'; import type { IFunctionParameterCollection } from './IFunctionParameterCollection';
import type { IFunctionParameter } from './IFunctionParameter'; import type { FunctionParameter } from './FunctionParameter';
export class FunctionParameterCollection implements IFunctionParameterCollection { export class FunctionParameterCollection implements IFunctionParameterCollection {
private parameters = new Array<IFunctionParameter>(); private parameters = new Array<FunctionParameter>();
public get all(): readonly IFunctionParameter[] { public get all(): readonly FunctionParameter[] {
return this.parameters; return this.parameters;
} }
public addParameter(parameter: IFunctionParameter) { public addParameter(parameter: FunctionParameter) {
this.ensureValidParameter(parameter); this.ensureValidParameter(parameter);
this.parameters.push(parameter); this.parameters.push(parameter);
} }
@@ -17,7 +17,7 @@ export class FunctionParameterCollection implements IFunctionParameterCollection
return this.parameters.find((existingParameter) => existingParameter.name === name); return this.parameters.find((existingParameter) => existingParameter.name === name);
} }
private ensureValidParameter(parameter: IFunctionParameter) { private ensureValidParameter(parameter: FunctionParameter) {
if (this.includesName(parameter.name)) { if (this.includesName(parameter.name)) {
throw new Error(`duplicate parameter name: "${parameter.name}"`); throw new Error(`duplicate parameter name: "${parameter.name}"`);
} }

View File

@@ -0,0 +1,21 @@
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,
};
};

View File

@@ -0,0 +1,9 @@
import type { FunctionParameter } from './FunctionParameter';
export interface IReadOnlyFunctionParameterCollection {
readonly all: readonly FunctionParameter[];
}
export interface IFunctionParameterCollection extends IReadOnlyFunctionParameterCollection {
addParameter(parameter: FunctionParameter): void;
}

View File

@@ -0,0 +1,22 @@
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}".`,
},
});
};

View File

@@ -2,66 +2,65 @@ import type {
FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData, FunctionData, CodeInstruction, CodeFunctionData, CallFunctionData,
CallInstruction, ParameterDefinitionData, CallInstruction, ParameterDefinitionData,
} from '@/application/collections/'; } from '@/application/collections/';
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { CodeValidator } from '@/application/Parser/Script/Validation/CodeValidator'; import { CodeValidator } from '@/application/Parser/Executable/Script/Validation/CodeValidator';
import { NoEmptyLines } from '@/application/Parser/Script/Validation/Rules/NoEmptyLines'; import { NoEmptyLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoEmptyLines';
import { NoDuplicatedLines } from '@/application/Parser/Script/Validation/Rules/NoDuplicatedLines'; import { NoDuplicatedLines } from '@/application/Parser/Executable/Script/Validation/Rules/NoDuplicatedLines';
import type { ICodeValidator } from '@/application/Parser/Script/Validation/ICodeValidator'; import type { ICodeValidator } from '@/application/Parser/Executable/Script/Validation/ICodeValidator';
import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers'; import { isArray, isNullOrUndefined, isPlainObject } from '@/TypeHelpers';
import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/ContextualError'; import { wrapErrorWithAdditionalContext, type ErrorWithContextWrapper } from '@/application/Parser/Common/ContextualError';
import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction'; import { createFunctionWithInlineCode, createCallerFunction } from './SharedFunction';
import { SharedFunctionCollection } from './SharedFunctionCollection'; import { SharedFunctionCollection } from './SharedFunctionCollection';
import { FunctionParameter } from './Parameter/FunctionParameter'; import { parseFunctionCalls, type FunctionCallsParser } from './Call/FunctionCallsParser';
import { parseFunctionCalls } from './Call/FunctionCallParser';
import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory'; import { createFunctionParameterCollection, type FunctionParameterCollectionFactory } from './Parameter/FunctionParameterCollectionFactory';
import { parseFunctionParameter, type FunctionParameterParser } from './Parameter/FunctionParameterParser';
import type { FunctionParameter } from './Parameter/FunctionParameter';
import type { ISharedFunctionCollection } from './ISharedFunctionCollection'; import type { ISharedFunctionCollection } from './ISharedFunctionCollection';
import type { ISharedFunctionsParser } from './ISharedFunctionsParser';
import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection'; import type { IReadOnlyFunctionParameterCollection } from './Parameter/IFunctionParameterCollection';
import type { ISharedFunction } from './ISharedFunction'; import type { ISharedFunction } from './ISharedFunction';
const DefaultSharedFunctionsParsingUtilities: SharedFunctionsParsingUtilities = { export interface SharedFunctionsParser {
wrapError: wrapErrorWithAdditionalContext, (
createParameter: (...args) => new FunctionParameter(...args),
codeValidator: CodeValidator.instance,
createParameterCollection: createFunctionParameterCollection,
};
export class SharedFunctionsParser implements ISharedFunctionsParser {
public static readonly instance: ISharedFunctionsParser = new SharedFunctionsParser();
constructor(
private readonly utilities = DefaultSharedFunctionsParsingUtilities,
) { }
public parseFunctions(
functions: readonly FunctionData[], functions: readonly FunctionData[],
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
): ISharedFunctionCollection { utilities?: SharedFunctionsParsingUtilities,
const collection = new SharedFunctionCollection(); ): ISharedFunctionCollection;
if (!functions.length) {
return collection;
}
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func, syntax, this.utilities))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
}, collection);
}
} }
export const parseSharedFunctions: SharedFunctionsParser = (
functions: readonly FunctionData[],
syntax: ILanguageSyntax,
utilities = DefaultUtilities,
) => {
const collection = new SharedFunctionCollection();
if (!functions.length) {
return collection;
}
ensureValidFunctions(functions);
return functions
.map((func) => parseFunction(func, syntax, utilities))
.reduce((acc, func) => {
acc.addFunction(func);
return acc;
}, collection);
};
const DefaultUtilities: SharedFunctionsParsingUtilities = {
wrapError: wrapErrorWithAdditionalContext,
parseParameter: parseFunctionParameter,
codeValidator: CodeValidator.instance,
createParameterCollection: createFunctionParameterCollection,
parseFunctionCalls,
};
interface SharedFunctionsParsingUtilities { interface SharedFunctionsParsingUtilities {
readonly wrapError: ErrorWithContextWrapper; readonly wrapError: ErrorWithContextWrapper;
readonly createParameter: FunctionParameterFactory; readonly parseParameter: FunctionParameterParser;
readonly codeValidator: ICodeValidator; readonly codeValidator: ICodeValidator;
readonly createParameterCollection: FunctionParameterCollectionFactory; readonly createParameterCollection: FunctionParameterCollectionFactory;
readonly parseFunctionCalls: FunctionCallsParser;
} }
export type FunctionParameterFactory = (
...args: ConstructorParameters<typeof FunctionParameter>
) => FunctionParameter;
function parseFunction( function parseFunction(
data: FunctionData, data: FunctionData,
syntax: ILanguageSyntax, syntax: ILanguageSyntax,
@@ -74,7 +73,7 @@ function parseFunction(
return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode); return createFunctionWithInlineCode(name, parameters, data.code, data.revertCode);
} }
// Has call // Has call
const calls = parseFunctionCalls(data.call); const calls = utilities.parseFunctionCalls(data.call);
return createCallerFunction(name, parameters, calls); return createCallerFunction(name, parameters, calls);
} }
@@ -98,7 +97,7 @@ function parseParameters(
utilities: SharedFunctionsParsingUtilities, utilities: SharedFunctionsParsingUtilities,
): IReadOnlyFunctionParameterCollection { ): IReadOnlyFunctionParameterCollection {
return (data.parameters || []) return (data.parameters || [])
.map((parameter) => createFunctionParameter( .map((parameter) => parseParameterWithContextualError(
data.name, data.name,
parameter, parameter,
utilities, utilities,
@@ -109,16 +108,13 @@ function parseParameters(
}, utilities.createParameterCollection()); }, utilities.createParameterCollection());
} }
function createFunctionParameter( function parseParameterWithContextualError(
functionName: string, functionName: string,
parameterData: ParameterDefinitionData, parameterData: ParameterDefinitionData,
utilities: SharedFunctionsParsingUtilities, utilities: SharedFunctionsParsingUtilities,
): FunctionParameter { ): FunctionParameter {
try { try {
return utilities.createParameter( return utilities.parseParameter(parameterData);
parameterData.name,
parameterData.optional || false,
);
} catch (err) { } catch (err) {
throw utilities.wrapError( throw utilities.wrapError(
err, err,

View File

@@ -1,7 +1,7 @@
import type { ScriptData } from '@/application/collections/'; import type { ScriptData } from '@/application/collections/';
import type { IScriptCode } from '@/domain/IScriptCode'; import type { ScriptCode } from '@/domain/Executables/Script/Code/ScriptCode';
export interface IScriptCompiler { export interface IScriptCompiler {
canCompile(script: ScriptData): boolean; canCompile(script: ScriptData): boolean;
compile(script: ScriptData): IScriptCode; compile(script: ScriptData): ScriptCode;
} }

View File

@@ -0,0 +1,87 @@
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 { 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 {
[compiledCode.code, compiledCode.revertCode]
.filter((code): code is string => Boolean(code))
.map((code) => code as string)
.forEach(
(code) => validator.throwIfInvalid(
code,
[new NoEmptyLines()],
),
);
}
function hasCall(data: ScriptData): data is ScriptData & CallInstruction {
return (data as CallInstruction).call !== undefined;
}

View File

@@ -0,0 +1,151 @@
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 { CollectionScript } from '@/domain/Executables/Script/CollectionScript';
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 { 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({
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,
) {
[scriptCode.execute, scriptCode.revert]
.filter((code): code is string => Boolean(code))
.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.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;
}
export type ScriptFactory = (
...parameters: ConstructorParameters<typeof CollectionScript>
) => Script;
const createScript: ScriptFactory = (...parameters) => {
return new CollectionScript(...parameters);
};
const DefaultUtilities: ScriptParserUtilities = {
levelParser: createEnumParser(RecommendationLevel),
createScript,
codeValidator: CodeValidator.instance,
wrapError: wrapErrorWithAdditionalContext,
createValidator: createExecutableDataValidator,
createCode: createScriptCode,
parseDocs,
};

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import type { ICodeLine } from '../ICodeLine'; import type { ICodeLine } from '../ICodeLine';
import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule'; import type { ICodeValidationRule, IInvalidCodeLine } from '../ICodeValidationRule';

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
const BatchFileCommonCodeParts = ['(', ')', 'else', '||']; const BatchFileCommonCodeParts = ['(', ')', 'else', '||'];
const PowerShellCommonCodeParts = ['{', '}']; const PowerShellCommonCodeParts = ['{', '}'];

View File

@@ -1,4 +1,4 @@
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
export class ShellScriptSyntax implements ILanguageSyntax { export class ShellScriptSyntax implements ILanguageSyntax {
public readonly commentDelimiters = ['#']; public readonly commentDelimiters = ['#'];

View File

@@ -1,6 +1,6 @@
import { ScriptingLanguage } from '@/domain/ScriptingLanguage'; import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory'; import { ScriptingLanguageFactory } from '@/application/Common/ScriptingLanguage/ScriptingLanguageFactory';
import type { ILanguageSyntax } from '@/application/Parser/Script/Validation/Syntax/ILanguageSyntax'; import type { ILanguageSyntax } from '@/application/Parser/Executable/Script/Validation/Syntax/ILanguageSyntax';
import { BatchFileSyntax } from './BatchFileSyntax'; import { BatchFileSyntax } from './BatchFileSyntax';
import { ShellScriptSyntax } from './ShellScriptSyntax'; import { ShellScriptSyntax } from './ShellScriptSyntax';
import type { ISyntaxFactory } from './ISyntaxFactory'; import type { ISyntaxFactory } from './ISyntaxFactory';

View File

@@ -0,0 +1,24 @@
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;
};

View File

@@ -0,0 +1,43 @@
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;
}

View File

@@ -0,0 +1,4 @@
export enum ExecutableType {
Script,
Category,
}

View File

@@ -0,0 +1,69 @@
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),
);
}
}

Some files were not shown because too many files have changed in this diff Show More