-
Notifications
You must be signed in to change notification settings - Fork 522
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
697 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
/*! | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import * as semver from 'semver' | ||
import globals from '../shared/extensionGlobals' | ||
import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification } from './types' | ||
|
||
/** | ||
* Evaluates if a given version fits into the parameters specified by a notification, e.g: | ||
* | ||
* extensionVersion: { | ||
* type: 'range', | ||
* lowerInclusive: '1.21.0' | ||
* } | ||
* | ||
* will match all versions 1.21.0 and up. | ||
* | ||
* @param version the version to check | ||
* @param condition the condition to check against | ||
* @returns true if the version satisfies the condition | ||
*/ | ||
function isValidVersion(version: string, condition: ConditionalClause): boolean { | ||
switch (condition.type) { | ||
case 'range': { | ||
const lowerConstraint = !condition.lowerInclusive || semver.gte(version, condition.lowerInclusive) | ||
const upperConstraint = !condition.upperExclusive || semver.lt(version, condition.upperExclusive) | ||
return lowerConstraint && upperConstraint | ||
} | ||
case 'exactMatch': | ||
return condition.values.some((v) => semver.eq(v, version)) | ||
case 'or': | ||
/** Check case where any of the subconditions are true, i.e. one of multiple range or exactMatch conditions */ | ||
return condition.clauses.some((clause) => isValidVersion(version, clause)) | ||
default: | ||
throw new Error(`Unknown clause type: ${(condition as any).type}`) | ||
} | ||
} | ||
|
||
/** | ||
* Determine whether or not to display a given notification based on whether the | ||
* notification requirements fit the extension context provided on initialization. | ||
* | ||
* Usage: | ||
* const myContext = { | ||
* extensionVersion: '4.5.6', | ||
* ... | ||
* } | ||
* | ||
* const ruleEngine = new RuleEngine(myContext) | ||
* | ||
* notifications.forEach(n => { | ||
* if (ruleEngine.shouldDisplayNotification(n)) { | ||
* // process notification | ||
* ... | ||
* } | ||
* }) | ||
* | ||
*/ | ||
export class RuleEngine { | ||
constructor(private readonly context: RuleContext) {} | ||
|
||
public shouldDisplayNotification(payload: ToolkitNotification) { | ||
return this.evaluate(payload.displayIf) | ||
} | ||
|
||
private evaluate(condition: DisplayIf): boolean { | ||
if (condition.extensionId !== globals.context.extension.id) { | ||
return false | ||
} | ||
|
||
if (condition.ideVersion) { | ||
if (!isValidVersion(this.context.ideVersion, condition.ideVersion)) { | ||
return false | ||
} | ||
} | ||
if (condition.extensionVersion) { | ||
if (!isValidVersion(this.context.extensionVersion, condition.extensionVersion)) { | ||
return false | ||
} | ||
} | ||
|
||
if (condition.additionalCriteria) { | ||
for (const criteria of condition.additionalCriteria) { | ||
if (!this.evaluateRule(criteria)) { | ||
return false | ||
} | ||
} | ||
} | ||
|
||
return true | ||
} | ||
|
||
private evaluateRule(criteria: CriteriaCondition) { | ||
const expected = criteria.values | ||
const expectedSet = new Set(expected) | ||
|
||
const isExpected = (i: string) => expectedSet.has(i) | ||
const hasAnyOfExpected = (i: string[]) => i.some((v) => expectedSet.has(v)) | ||
const isSuperSetOfExpected = (i: string[]) => { | ||
const s = new Set(i) | ||
return expected.every((v) => s.has(v)) | ||
} | ||
const isEqualSetToExpected = (i: string[]) => { | ||
const s = new Set(i) | ||
return expected.every((v) => s.has(v)) && i.every((v) => expectedSet.has(v)) | ||
} | ||
|
||
// Maybe we could abstract these out into some strategy pattern with classes. | ||
// But this list is short and its unclear if we need to expand it further. | ||
// Also, we might replace this with a common implementation amongst the toolkits. | ||
// So... YAGNI | ||
switch (criteria.type) { | ||
case 'OS': | ||
return isExpected(this.context.os) | ||
case 'ComputeEnv': | ||
return isExpected(this.context.computeEnv) | ||
case 'AuthType': | ||
return hasAnyOfExpected(this.context.authTypes) | ||
case 'AuthRegion': | ||
return hasAnyOfExpected(this.context.authRegions) | ||
case 'AuthState': | ||
return hasAnyOfExpected(this.context.authStates) | ||
case 'AuthScopes': | ||
return isEqualSetToExpected(this.context.authScopes) | ||
case 'InstalledExtensions': | ||
return isSuperSetOfExpected(this.context.installedExtensions) | ||
case 'ActiveExtensions': | ||
return isSuperSetOfExpected(this.context.activeExtensions) | ||
default: | ||
throw new Error(`Unknown criteria type: ${criteria.type}`) | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
/*! | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* SPDX-License-Identifier: Apache-2.0 | ||
*/ | ||
|
||
import * as vscode from 'vscode' | ||
import { EnvType, OperatingSystem } from '../shared/telemetry/util' | ||
|
||
/** Types of information that we can use to determine whether to show a notification or not. */ | ||
export type Criteria = | ||
| 'OS' | ||
| 'ComputeEnv' | ||
| 'AuthType' | ||
| 'AuthRegion' | ||
| 'AuthState' | ||
| 'AuthScopes' | ||
| 'InstalledExtensions' | ||
| 'ActiveExtensions' | ||
|
||
/** Generic condition where the type determines how the values are evaluated. */ | ||
export interface CriteriaCondition { | ||
readonly type: Criteria | ||
readonly values: string[] | ||
} | ||
|
||
/** One of the subconditions (clauses) must match to be valid. */ | ||
export interface OR { | ||
readonly type: 'or' | ||
readonly clauses: (Range | ExactMatch)[] | ||
} | ||
|
||
/** Version must be within the bounds to be valid. Missing bound indicates that bound is open-ended. */ | ||
export interface Range { | ||
readonly type: 'range' | ||
readonly lowerInclusive?: string // null means "-inf" | ||
readonly upperExclusive?: string // null means "+inf" | ||
} | ||
|
||
/** Version must be equal. */ | ||
export interface ExactMatch { | ||
readonly type: 'exactMatch' | ||
readonly values: string[] | ||
} | ||
|
||
export type ConditionalClause = Range | ExactMatch | OR | ||
|
||
/** How to display the notification. */ | ||
export interface UIRenderInstructions { | ||
content: { | ||
[`en-US`]: { | ||
title: string | ||
description: string | ||
} | ||
} | ||
// TODO actions | ||
} | ||
|
||
/** Condition/criteria section of a notification. */ | ||
export interface DisplayIf { | ||
extensionId: string | ||
ideVersion?: ConditionalClause | ||
extensionVersion?: ConditionalClause | ||
additionalCriteria?: CriteriaCondition[] | ||
} | ||
|
||
export interface ToolkitNotification { | ||
id: string | ||
displayIf: DisplayIf | ||
uiRenderInstructions: UIRenderInstructions | ||
} | ||
|
||
export interface Notifications { | ||
schemaVersion: string | ||
notifications: ToolkitNotification[] | ||
} | ||
|
||
export interface RuleContext { | ||
readonly ideVersion: typeof vscode.version | ||
readonly extensionVersion: string | ||
readonly os: OperatingSystem | ||
readonly computeEnv: EnvType | ||
readonly authTypes: string[] | ||
readonly authRegions: string[] | ||
readonly authStates: string[] | ||
readonly authScopes: string[] | ||
readonly installedExtensions: string[] | ||
readonly activeExtensions: string[] | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.