Skip to content

Commit

Permalink
Add unique script/category IDs $49, $59, $262, $126
Browse files Browse the repository at this point in the history
This commit is too big, all "supporting changes " should be committed in
different commits:

1. Commit I. `Executable` domain concept.
  - Rename `Node` to `Executable` throughout the codebase.
  - Create subfolders in `src/domain` for better structure/navigatability.
  - Ensure TODO.md is addressed and removed from this commit.
3. Commit II (SMALL):
  Search `TODO: In separate commit trim this data.`, fix it.
2. Commit II. Identifiable pattern (or merge with commit III)?
  - Use string arbitrary IDs for categories.
  - Use string IDs for categories instead of using numeric pseudo-IDs.
3. Commit III. Entity
  - Remove Entity from infrastructure layer to better align with DDD
    (domain-driven design) as it's a domain concern.
  - Remove EntityBase for simplicity and to avoid technical nomenclature
    in names.

TODO:
- Addd valitaioon to ensure that every script / category has unique ID across collections.
- Bug: Standard -> Selected _-> None, when done Standard not being higlighted.
- Search `TODO:` fix all.

This commit introduces IDs for all categories and scripts. The design
provides a foundation for future expansion.
Using IDs simplify planned import / export functionality.

Key changes:

- Add `id:` property for script and categories and parse them in the
  application.
- Add ID validation in collection data with suggestions.
- Add utility/helper script to generate missing IDs in collection files.

Supporting changes:

- Name related interfaces to drop `I` prefix to align with modern
  conventions.
- Remove Entity from infrastructure layer to better align with DDD
  (domain-driven design) as it's a domain concern.
- Remove EntityBase for simplicity and to avoid technical nomenclature
  in names.
- Introduce new types for keys and identifiable objects.
- Create subfolders in `src/domain` for better structure/navigatability.
- Improve context on thrown error messages.
- Use string IDs for categories instead of using numeric pseudo-IDs.
- Add more test cases for new functionality and extend existing tests to
  ensure no functionality is broken.
  • Loading branch information
undergroundwires committed May 28, 2024
1 parent 22d6c79 commit fc9b282
Show file tree
Hide file tree
Showing 141 changed files with 3,225 additions and 1,105 deletions.
15 changes: 15 additions & 0 deletions .github/workflows/checks.scripts.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -85,3 +85,18 @@ jobs:
-
name: Configure VSCode
run: python3 ./scripts/configure_vscode.py

add_missing_ids:
runs-on: ${{ matrix.os }}-latest
strategy:
matrix:
os: [ macos, ubuntu, windows ]
steps:
-
name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.x'
-
name: Run the script
run: python3 ./scripts/add_missing_ids.py
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -558,7 +558,7 @@
* rework Cortana scripts to remove duplicates, better document and support Windows version 2004/2009 #43 | [7cc161c](https://github.com/undergroundwires/privacy.sexy/commit/7cc161c828a3fa49f6f254e31834a95a502b7aa2)
* rename Application to CategoryCollection #40 | [6fe858d](https://github.com/undergroundwires/privacy.sexy/commit/6fe858d86aeb0f8b6d5ae5c2a5e3c25ff32e5f6f)
* add script to clean previous windows installation #35 | [3455a2c](https://github.com/undergroundwires/privacy.sexy/commit/3455a2ca6ce13f9b0e866d88532a5c3d6de30d4d)
* refactor to allow switching ICategoryCollection context #40 | [2e40605](https://github.com/undergroundwires/privacy.sexy/commit/2e40605d59eb764768457c6af561487e7ff09777)
* refactor to allow switching CategoryCollection context #40 | [2e40605](https://github.com/undergroundwires/privacy.sexy/commit/2e40605d59eb764768457c6af561487e7ff09777)
* fix typo causing uninstalling capabilities to fail #51 | [c299e95](https://github.com/undergroundwires/privacy.sexy/commit/c299e95bc6d588317b69a9efcf5752ff5c9c3926)
* improve uninstalling apps to show errors and exit if taking ownership fails #51 | [72e925f](https://github.com/undergroundwires/privacy.sexy/commit/72e925fb6f908cd58fb50618f29726b3fb54a7f1)
* move application.yaml to collections/windows.yaml #40 | [6b83dcb](https://github.com/undergroundwires/privacy.sexy/commit/6b83dcbf8fa08b4efe9974c7d7a667458f7c595c)
Expand Down
6 changes: 5 additions & 1 deletion docs/collection-files.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@ Represents a logical group of scripts and subcategories.

#### `Category` syntax

- `id:`: *string* **(required)**
- Each ID must be unique, distinguishing each script and category within the same collection.
- `category:` *`string`* **(required)**
- Name of the category.
- ❗ Must be unique throughout the [collection](#collection).
Expand All @@ -45,7 +47,7 @@ Represents a logical group of scripts and subcategories.

### `Script`

Represents an individual tweak.
Represents an independently executable and reversible tweak.

Types (like [functions](#function)):

Expand All @@ -60,6 +62,8 @@ Types (like [functions](#function)):

#### `Script` syntax

- `id:`: *string* **(required)**
- Each ID must be unique, distinguishing each script and category within the same collection.
- `name`: *`string`* **(required)**
- Script name.
- ❗ Must be unique throughout the [Collection](#collection).
Expand Down
4 changes: 3 additions & 1 deletion docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,10 @@ See [ci-cd.md](./ci-cd.md) for more information.
- [**`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.
- 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.
- [**`python3 ./scripts/add_missing_ids.py`**](../scripts/add_missing_ids.py):
- Adds unique `id` fields to scripts and categories lacking them in collection files.

#### Automation scripts

Expand Down
126 changes: 126 additions & 0 deletions scripts/add_missing_ids.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
"""
Description:
This script checks the existing collection files and add `id` field to missing script/categories
by generating a new unique id.
Usage:
python3 ./scripts/add_missing_ids.py
Note:
This command must run from the project root directory.
"""
# pylint: disable=missing-function-docstring,missing-class-docstring

import os
from pathlib import Path
import re
import sys
from typing import List, Set
import uuid

def main() -> None:
collections_directory = './src/application/collections'
yaml_file_paths = find_yaml_files(collections_directory)
if not yaml_file_paths:
print('No collection files found', file=sys.stderr)
sys.exit(1)
print(f'Total collection files found: {len(yaml_file_paths)}.')

for yaml_file_path in yaml_file_paths:
print(f'Processing: {yaml_file_path}')
old_contents = read_yaml_file(yaml_file_path)
processed_yaml = add_id_to_yaml_content(old_contents)
if not processed_yaml.total_new_ids:
print('👌 No changes needed, no missing IDs.')
continue
save_yaml_file(yaml_file_path, processed_yaml.new_content)
print(f'🆕 Added missing IDs (total: {processed_yaml.total_new_ids}).')

class YamlIdAdditionResult:
def __init__(self, new_content: str, total_new_ids: int):
self.new_content = new_content
self.total_new_ids = total_new_ids

def add_id_to_yaml_content(content: str) -> YamlIdAdditionResult:
# Avoiding external yaml libraries to preserve YAML structure and comments.
existing_ids = find_existing_ids(content)
original_lines = content.splitlines()
new_lines = []
total_new_ids = 0
functions_section_reached = False
for i, line in enumerate(original_lines):
if is_functions_section_start(line):
functions_section_reached = True
if (not functions_section_reached) and \
is_executable_name(line) and \
(not has_line_before_defined_id(i, original_lines)):
indentation = len(line) - len(line.lstrip())
new_id = generate_unique_id(existing_ids)
total_new_ids += 1
existing_ids.add(new_id)
id_line_prefix = "#" if is_preceding_line_comment(i, original_lines) else ""
id_line = f'{" " * indentation}{id_line_prefix}id: {new_id}'
new_lines.append(id_line) # Insert new id line before 'name:'
new_lines.append(line)
return YamlIdAdditionResult(
new_content=os.linesep.join(new_lines),
total_new_ids=total_new_ids,
)

def is_functions_section_start(collection_file_line: str) -> bool:
return 'functions:' in collection_file_line

def is_executable_name(collection_file_line: str) -> bool:
non_commented_line = get_non_commented_line(collection_file_line)
stripped_line = non_commented_line.lstrip()
return stripped_line.startswith('name:') or stripped_line.startswith('category:')

def is_preceding_line_comment(current_line: int, all_lines: list[str]) -> bool:
return current_line > 0 and all_lines[current_line-1].strip().startswith('#')

def has_line_before_defined_id(current_line: int, all_lines: list[str]) -> bool:
if current_line == 0:
return False
line_before = all_lines[current_line-1]
non_commented_line = get_non_commented_line(line_before)
stripped_line = non_commented_line.lstrip()
return stripped_line.startswith('id:')

def save_yaml_file(absolute_file_path: str, new_contents: str) -> None:
with open(absolute_file_path, 'w', encoding='utf-8') as file:
file.write(new_contents)

def read_yaml_file(absolute_file_path: str) -> str:
with open(absolute_file_path, 'r', encoding='utf-8') as file:
return file.read()

def find_yaml_files(directory_path: str) -> List[str]:
return list(Path(directory_path).glob('**/*.yaml'))

def find_existing_ids(content: str) -> Set[str]:
pattern = r'^\s*#?\s*id:\s*(\S+)' # Matches 'id:' lines including commented lines
return set(re.findall(pattern, content, re.MULTILINE))

def get_non_commented_line(yaml_line: str) -> str:
pattern = re.compile(r'^\s*#\s?(.*)$')
match = pattern.match(yaml_line)
return match.group(1) if match else yaml_line

def generate_unique_id(generated_ids: Set[str]) -> str:
new_id = generate_new_id()
while new_id in generated_ids:
new_id = generate_new_id()
return new_id

def generate_new_id() -> str:
partial_uuid = str(uuid.uuid4()).split('-', maxsplit=1)[0]
if is_numeric(partial_uuid): # Creates issues with yaml parsing, yaml considering it as a number
return generate_new_id()
return partial_uuid

def is_numeric(string: str) -> bool:
numeric_pattern = re.compile(r'^\d+$|^[+-]?\d+\.?\d*[eE][+-]?\d+$')
return numeric_pattern.match(string)

if __name__ == "__main__":
main()
4 changes: 2 additions & 2 deletions src/application/Context/ApplicationContext.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IApplication } from '@/domain/IApplication';
import { OperatingSystem } from '@/domain/OperatingSystem';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import { EventSource } from '@/infrastructure/Events/EventSource';
import { assertInRange } from '@/application/Common/Enum';
import { CategoryCollectionState } from './State/CategoryCollectionState';
Expand All @@ -12,7 +12,7 @@ type StateMachine = Map<OperatingSystem, ICategoryCollectionState>;
export class ApplicationContext implements IApplicationContext {
public readonly contextChanged = new EventSource<IApplicationContextChangedEvent>();

public collection: ICategoryCollection;
public collection: CategoryCollection;

public currentOs: OperatingSystem;

Expand Down
4 changes: 2 additions & 2 deletions src/application/Context/State/CategoryCollectionState.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { AdaptiveFilterContext } from './Filter/AdaptiveFilterContext';
import { ApplicationCode } from './Code/ApplicationCode';
Expand All @@ -18,7 +18,7 @@ export class CategoryCollectionState implements ICategoryCollectionState {
public readonly filter: FilterContext;

public constructor(
public readonly collection: ICategoryCollection,
public readonly collection: CategoryCollection,
selectionFactory = DefaultSelectionFactory,
codeFactory = DefaultCodeFactory,
filterFactory = DefaultFilterFactory,
Expand Down
27 changes: 14 additions & 13 deletions src/application/Context/State/Code/Event/CodeChangedEvent.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
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 { SelectedScript } from '@/application/Context/State/Selection/Script/SelectedScript';
import type { ExecutableKey } from '@/domain/Executables/ExecutableKey/ExecutableKey';
import type { ICodeChangedEvent } from './ICodeChangedEvent';

export class CodeChangedEvent implements ICodeChangedEvent {
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(
code: string,
Expand All @@ -25,7 +26,7 @@ export class CodeChangedEvent implements ICodeChangedEvent {
this.addedScripts = selectIfNotExists(newScripts, oldScripts);
this.removedScripts = selectIfNotExists(oldScripts, newScripts);
this.changedScripts = getChangedScripts(oldScripts, newScripts);
this.scripts = new Map<IScript, ICodePosition>();
this.scripts = new Map<Script, ICodePosition>();
scripts.forEach((position, selection) => {
this.scripts.set(selection.script, position);
});
Expand All @@ -35,13 +36,13 @@ export class CodeChangedEvent implements ICodeChangedEvent {
return this.scripts.size === 0;
}

public getScriptPositionInCode(script: IScript): ICodePosition {
return this.getPositionById(script.id);
public getScriptPositionInCode(script: Script): ICodePosition {
return this.getPositionById(script.key);
}

private getPositionById(scriptId: string): ICodePosition {
private getPositionById(scriptKey: ExecutableKey): ICodePosition {
const position = [...this.scripts.entries()]
.filter(([s]) => s.id === scriptId)
.filter(([s]) => s.key.equals(scriptKey))
.map(([, pos]) => pos)
.at(0);
if (!position) {
Expand All @@ -65,9 +66,9 @@ function ensureAllPositionsExist(script: string, positions: ReadonlyArray<ICodeP
function getChangedScripts(
oldScripts: ReadonlyArray<SelectedScript>,
newScripts: ReadonlyArray<SelectedScript>,
): ReadonlyArray<IScript> {
): ReadonlyArray<Script> {
return newScripts
.filter((newScript) => oldScripts.find((oldScript) => oldScript.id === newScript.id
.filter((newScript) => oldScripts.find((oldScript) => oldScript.key === newScript.key
&& oldScript.revert !== newScript.revert))
.map((selection) => selection.script);
}
Expand All @@ -77,6 +78,6 @@ function selectIfNotExists(
test: ReadonlyArray<SelectedScript>,
) {
return selectableContainer
.filter((script) => !test.find((oldScript) => oldScript.id === script.id))
.filter((script) => !test.find((oldScript) => oldScript.key === script.key))
.map((selection) => selection.script);
}
10 changes: 5 additions & 5 deletions src/application/Context/State/Code/Event/ICodeChangedEvent.ts
Original file line number Diff line number Diff line change
@@ -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';

export interface ICodeChangedEvent {
readonly code: string;
readonly addedScripts: ReadonlyArray<IScript>;
readonly removedScripts: ReadonlyArray<IScript>;
readonly changedScripts: ReadonlyArray<IScript>;
readonly addedScripts: ReadonlyArray<Script>;
readonly removedScripts: ReadonlyArray<Script>;
readonly changedScripts: ReadonlyArray<Script>;
isEmpty(): boolean;
getScriptPositionInCode(script: IScript): ICodePosition;
getScriptPositionInCode(script: Script): ICodePosition;
}
4 changes: 2 additions & 2 deletions src/application/Context/State/Filter/AdaptiveFilterContext.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventSource } from '@/infrastructure/Events/EventSource';
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import { FilterChange } from './Event/FilterChange';
import { LinearFilterStrategy } from './Strategy/LinearFilterStrategy';
import type { FilterResult } from './Result/FilterResult';
Expand All @@ -13,7 +13,7 @@ export class AdaptiveFilterContext implements FilterContext {
public currentFilter: FilterResult | undefined;

constructor(
private readonly collection: ICategoryCollection,
private readonly collection: CategoryCollection,
private readonly filterStrategy: FilterStrategy = new LinearFilterStrategy(),
) {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { IScript } from '@/domain/IScript';
import type { ICategory } from '@/domain/ICategory';
import type { Script } from '@/domain/Executables/Script/Script';
import type { Category } from '@/domain/Executables/Category/Category';
import type { FilterResult } from './FilterResult';

export class AppliedFilterResult implements FilterResult {
constructor(
public readonly scriptMatches: ReadonlyArray<IScript>,
public readonly categoryMatches: ReadonlyArray<ICategory>,
public readonly scriptMatches: ReadonlyArray<Script>,
public readonly categoryMatches: ReadonlyArray<Category>,
public readonly query: string,
) {
if (!query) { throw new Error('Query is empty or undefined'); }
Expand Down
7 changes: 4 additions & 3 deletions src/application/Context/State/Filter/Result/FilterResult.ts
Original file line number Diff line number Diff line change
@@ -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 {
readonly categoryMatches: ReadonlyArray<ICategory>;
readonly scriptMatches: ReadonlyArray<IScript>;
readonly categoryMatches: ReadonlyArray<Category>;
readonly scriptMatches: ReadonlyArray<Script>;
readonly query: string;
hasAnyMatches(): boolean;
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { ICategoryCollection } from '@/domain/ICategoryCollection';
import type { CategoryCollection } from '@/domain/Collection/CategoryCollection';
import type { FilterResult } from '../Result/FilterResult';

export interface FilterStrategy {
applyFilter(
filter: string,
collection: ICategoryCollection,
collection: CategoryCollection,
): FilterResult;
}
Loading

0 comments on commit fc9b282

Please sign in to comment.