Skip to content

Commit

Permalink
add support for different recommendation levels: strict and standard
Browse files Browse the repository at this point in the history
  • Loading branch information
undergroundwires committed Oct 19, 2020
1 parent 978bab0 commit 14be301
Show file tree
Hide file tree
Showing 20 changed files with 951 additions and 651 deletions.
20 changes: 19 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,25 @@
### Extend scripts

- Create a [pull request](#Pull-Request-Process) for [application.yaml](./src/application/application.yaml)
- 🙏 For any new script, try to add `revertCode` that'll revert the changes caused by the script.
- 🙏 For any new script, please add `revertCode` and `docs` values if possible.
- Structure of `script` object:
- `name`: *`string`* (**required**)
- Name of the script
- E.g. `Disable targeted ads`
- `code`: *`string`* (**required**)
- Batch file commands that will be executed
- `docs`: *`string`* | `[ string, ... ]`
- Documentation URL or list of URLs for those who wants to learn more about the script
- E.g. `https://docs.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_telemetry`
- `revertCode`: `string`
- Code that'll undo the change done by `code` property.
- E.g. let's say `code` sets an environment variable as `setx POWERSHELL_TELEMETRY_OPTOUT 1`
- then `revertCode` should be doing `setx POWERSHELL_TELEMETRY_OPTOUT 0`
- `recommend`: `"standard"` | `"strict"` | `undefined` (default)
- If not defined then the script will not be recommended
- If defined it can be either
- `standard`: Will be recommended for general users
- `strict`: Will only be recommended with a warning
- See [typings](./src/application/application.yaml.d.ts) for documentation as code.

### Handle the state in presentation layer
Expand Down
18 changes: 17 additions & 1 deletion src/application/Parser/ScriptParser.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Script } from '@/domain/Script';
import { YamlScript } from 'js-yaml-loader!./application.yaml';
import { parseDocUrls } from './DocumentationParser';
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';

export function parseScript(yamlScript: YamlScript): Script {
if (!yamlScript) {
Expand All @@ -11,6 +12,21 @@ export function parseScript(yamlScript: YamlScript): Script {
/* code */ yamlScript.code,
/* revertCode */ yamlScript.revertCode,
/* docs */ parseDocUrls(yamlScript),
/* isRecommended */ yamlScript.recommend);
/* level */ getLevel(yamlScript.recommend));
return script;
}

function getLevel(level: string): RecommendationLevel | undefined {
if (!level) {
return undefined;
}
if (typeof level !== 'string') {
throw new Error(`level must be a string but it was ${typeof level}`);
}
const typedLevel = RecommendationLevelNames
.find((l) => l.toLowerCase() === level.toLowerCase());
if (!typedLevel) {
throw new Error(`unknown level: \"${level}\"`);
}
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
}
532 changes: 234 additions & 298 deletions src/application/application.yaml

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/application/application.yaml.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ declare module 'js-yaml-loader!*' {
name: string;
code: string;
revertCode: string;
recommend: boolean;
recommend: string | undefined;
}

export interface YamlCategory extends YamlDocumentable {
Expand Down
109 changes: 73 additions & 36 deletions src/domain/Application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,43 +3,50 @@ import { ICategory } from './ICategory';
import { IScript } from './IScript';
import { IApplication } from './IApplication';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel, RecommendationLevelNames, RecommendationLevels } from './RecommendationLevel';

export class Application implements IApplication {
public get totalScripts(): number { return this.flattened.allScripts.length; }
public get totalCategories(): number { return this.flattened.allCategories.length; }
public get totalScripts(): number { return this.queryable.allScripts.length; }
public get totalCategories(): number { return this.queryable.allCategories.length; }

private readonly flattened: IFlattenedApplication;
private readonly queryable: IQueryableApplication;

constructor(
public readonly info: IProjectInformation,
public readonly actions: ReadonlyArray<ICategory>) {
if (!info) {
throw new Error('info is undefined');
}
this.flattened = flatten(actions);
ensureValid(this.flattened);
ensureNoDuplicates(this.flattened.allCategories);
ensureNoDuplicates(this.flattened.allScripts);
this.queryable = makeQueryable(actions);
ensureValid(this.queryable);
ensureNoDuplicates(this.queryable.allCategories);
ensureNoDuplicates(this.queryable.allScripts);
}

public findCategory(categoryId: number): ICategory | undefined {
return this.flattened.allCategories.find((category) => category.id === categoryId);
return this.queryable.allCategories.find((category) => category.id === categoryId);
}

public getRecommendedScripts(): readonly IScript[] {
return this.flattened.allScripts.filter((script) => script.isRecommended);
public getScriptsByLevel(level: RecommendationLevel): readonly IScript[] {
if (isNaN(level)) {
throw new Error('undefined level');
}
if (!(level in RecommendationLevel)) {
throw new Error(`invalid level: ${level}`);
}
return this.queryable.scriptsByLevel.get(level);
}

public findScript(scriptId: string): IScript | undefined {
return this.flattened.allScripts.find((script) => script.id === scriptId);
return this.queryable.allScripts.find((script) => script.id === scriptId);
}

public getAllScripts(): IScript[] {
return this.flattened.allScripts;
return this.queryable.allScripts;
}

public getAllCategories(): ICategory[] {
return this.flattened.allCategories;
return this.queryable.allCategories;
}
}

Expand All @@ -61,55 +68,85 @@ function ensureNoDuplicates<TKey>(entities: ReadonlyArray<IEntity<TKey>>) {
}
}

interface IFlattenedApplication {
interface IQueryableApplication {
allCategories: ICategory[];
allScripts: IScript[];
scriptsByLevel: Map<RecommendationLevel, readonly IScript[]>;
}

function ensureValid(application: IQueryableApplication) {
ensureValidCategories(application.allCategories);
ensureValidScripts(application.allScripts);
}

function ensureValid(application: IFlattenedApplication) {
if (!application.allCategories || application.allCategories.length === 0) {
function ensureValidCategories(allCategories: readonly ICategory[]) {
if (!allCategories || allCategories.length === 0) {
throw new Error('Application must consist of at least one category');
}
if (!application.allScripts || application.allScripts.length === 0) {
}

function ensureValidScripts(allScripts: readonly IScript[]) {
if (!allScripts || allScripts.length === 0) {
throw new Error('Application must consist of at least one script');
}
if (application.allScripts.filter((script) => script.isRecommended).length === 0) {
throw new Error('Application must consist of at least one recommended script');
for (const level of RecommendationLevels) {
if (allScripts.every((script) => script.level !== level)) {
throw new Error(`none of the scripts are recommended as ${RecommendationLevel[level]}`);
}
}
}

function flattenApplication(categories: ReadonlyArray<ICategory>): [ICategory[], IScript[]] {
const allCategories = new Array<ICategory>();
const allScripts = new Array<IScript>();
flattenCategories(categories, allCategories, allScripts);
return [
allCategories,
allScripts,
];
}

function flattenCategories(
categories: ReadonlyArray<ICategory>,
flattened: IFlattenedApplication): IFlattenedApplication {
allCategories: ICategory[],
allScripts: IScript[]): IQueryableApplication {
if (!categories || categories.length === 0) {
return flattened;
return;
}
for (const category of categories) {
flattened.allCategories.push(category);
flattened = flattenScripts(category.scripts, flattened);
flattened = flattenCategories(category.subCategories, flattened);
allCategories.push(category);
flattenScripts(category.scripts, allScripts);
flattenCategories(category.subCategories, allCategories, allScripts);
}
return flattened;
}

function flattenScripts(
scripts: ReadonlyArray<IScript>,
flattened: IFlattenedApplication): IFlattenedApplication {
allScripts: IScript[]): IScript[] {
if (!scripts) {
return flattened;
return;
}
for (const script of scripts) {
flattened.allScripts.push(script);
allScripts.push(script);
}
return flattened;
}

function flatten(
categories: ReadonlyArray<ICategory>): IFlattenedApplication {
let flattened: IFlattenedApplication = {
allCategories: new Array<ICategory>(),
allScripts: new Array<IScript>(),
function makeQueryable(
actions: ReadonlyArray<ICategory>): IQueryableApplication {
const flattened = flattenApplication(actions);
return {
allCategories: flattened[0],
allScripts: flattened[1],
scriptsByLevel: groupByLevel(flattened[1]),
};
flattened = flattenCategories(categories, flattened);
return flattened;
}

function groupByLevel(allScripts: readonly IScript[]): Map<RecommendationLevel, readonly IScript[]> {
const map = new Map<RecommendationLevel, readonly IScript[]>();
for (const levelName of RecommendationLevelNames) {
const level = RecommendationLevel[levelName];
const scripts = allScripts.filter((script) => script.level !== undefined && script.level <= level);
map.set(level, scripts);
}
return map;
}
3 changes: 2 additions & 1 deletion src/domain/IApplication.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { IScript } from '@/domain/IScript';
import { ICategory } from '@/domain/ICategory';
import { IProjectInformation } from './IProjectInformation';
import { RecommendationLevel } from './RecommendationLevel';

export interface IApplication {
readonly info: IProjectInformation;
readonly totalScripts: number;
readonly totalCategories: number;
readonly actions: ReadonlyArray<ICategory>;

getRecommendedScripts(): ReadonlyArray<IScript>;
getScriptsByLevel(level: RecommendationLevel): ReadonlyArray<IScript>;
findCategory(categoryId: number): ICategory | undefined;
findScript(scriptId: string): IScript | undefined;
getAllScripts(): ReadonlyArray<IScript>;
Expand Down
3 changes: 2 additions & 1 deletion src/domain/IScript.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { IEntity } from '../infrastructure/Entity/IEntity';
import { IDocumentable } from './IDocumentable';
import { RecommendationLevel } from './RecommendationLevel';

export interface IScript extends IEntity<string>, IDocumentable {
readonly name: string;
readonly isRecommended: boolean;
readonly level?: RecommendationLevel;
readonly documentationUrls: ReadonlyArray<string>;
readonly code: string;
readonly revertCode: string;
Expand Down
11 changes: 11 additions & 0 deletions src/domain/RecommendationLevel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export enum RecommendationLevel {
Standard = 0,
Strict = 1,
}

export const RecommendationLevelNames = Object
.values(RecommendationLevel)
.filter((level) => typeof level === 'string') as string[];

export const RecommendationLevels = RecommendationLevelNames
.map((level) => RecommendationLevel[level]) as RecommendationLevel[];
10 changes: 9 additions & 1 deletion src/domain/Script.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,17 @@
import { BaseEntity } from '@/infrastructure/Entity/BaseEntity';
import { IScript } from './IScript';
import { RecommendationLevel } from './RecommendationLevel';

export class Script extends BaseEntity<string> implements IScript {
constructor(
public readonly name: string,
public readonly code: string,
public readonly revertCode: string,
public readonly documentationUrls: ReadonlyArray<string>,
public readonly isRecommended: boolean) {
public readonly level?: RecommendationLevel) {
super(name);
validateCode(name, code);
validateLevel(level);
if (revertCode) {
validateCode(name, revertCode);
if (code === revertCode) {
Expand All @@ -22,6 +24,12 @@ export class Script extends BaseEntity<string> implements IScript {
}
}

function validateLevel(level?: RecommendationLevel) {
if (level !== undefined && !(level in RecommendationLevel)) {
throw new Error(`invalid level: ${level}`);
}
}

function validateCode(name: string, code: string): void {
if (!code || code.length === 0) {
throw new Error(`Code of ${name} is empty or null`);
Expand Down
Loading

0 comments on commit 14be301

Please sign in to comment.