Skip to content

Commit e41038e

Browse files
Merge master into feature/emr
2 parents e556998 + b958880 commit e41038e

File tree

4 files changed

+697
-2
lines changed

4 files changed

+697
-2
lines changed
Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as semver from 'semver'
7+
import globals from '../shared/extensionGlobals'
8+
import { ConditionalClause, RuleContext, DisplayIf, CriteriaCondition, ToolkitNotification } from './types'
9+
10+
/**
11+
* Evaluates if a given version fits into the parameters specified by a notification, e.g:
12+
*
13+
* extensionVersion: {
14+
* type: 'range',
15+
* lowerInclusive: '1.21.0'
16+
* }
17+
*
18+
* will match all versions 1.21.0 and up.
19+
*
20+
* @param version the version to check
21+
* @param condition the condition to check against
22+
* @returns true if the version satisfies the condition
23+
*/
24+
function isValidVersion(version: string, condition: ConditionalClause): boolean {
25+
switch (condition.type) {
26+
case 'range': {
27+
const lowerConstraint = !condition.lowerInclusive || semver.gte(version, condition.lowerInclusive)
28+
const upperConstraint = !condition.upperExclusive || semver.lt(version, condition.upperExclusive)
29+
return lowerConstraint && upperConstraint
30+
}
31+
case 'exactMatch':
32+
return condition.values.some((v) => semver.eq(v, version))
33+
case 'or':
34+
/** Check case where any of the subconditions are true, i.e. one of multiple range or exactMatch conditions */
35+
return condition.clauses.some((clause) => isValidVersion(version, clause))
36+
default:
37+
throw new Error(`Unknown clause type: ${(condition as any).type}`)
38+
}
39+
}
40+
41+
/**
42+
* Determine whether or not to display a given notification based on whether the
43+
* notification requirements fit the extension context provided on initialization.
44+
*
45+
* Usage:
46+
* const myContext = {
47+
* extensionVersion: '4.5.6',
48+
* ...
49+
* }
50+
*
51+
* const ruleEngine = new RuleEngine(myContext)
52+
*
53+
* notifications.forEach(n => {
54+
* if (ruleEngine.shouldDisplayNotification(n)) {
55+
* // process notification
56+
* ...
57+
* }
58+
* })
59+
*
60+
*/
61+
export class RuleEngine {
62+
constructor(private readonly context: RuleContext) {}
63+
64+
public shouldDisplayNotification(payload: ToolkitNotification) {
65+
return this.evaluate(payload.displayIf)
66+
}
67+
68+
private evaluate(condition: DisplayIf): boolean {
69+
if (condition.extensionId !== globals.context.extension.id) {
70+
return false
71+
}
72+
73+
if (condition.ideVersion) {
74+
if (!isValidVersion(this.context.ideVersion, condition.ideVersion)) {
75+
return false
76+
}
77+
}
78+
if (condition.extensionVersion) {
79+
if (!isValidVersion(this.context.extensionVersion, condition.extensionVersion)) {
80+
return false
81+
}
82+
}
83+
84+
if (condition.additionalCriteria) {
85+
for (const criteria of condition.additionalCriteria) {
86+
if (!this.evaluateRule(criteria)) {
87+
return false
88+
}
89+
}
90+
}
91+
92+
return true
93+
}
94+
95+
private evaluateRule(criteria: CriteriaCondition) {
96+
const expected = criteria.values
97+
const expectedSet = new Set(expected)
98+
99+
const isExpected = (i: string) => expectedSet.has(i)
100+
const hasAnyOfExpected = (i: string[]) => i.some((v) => expectedSet.has(v))
101+
const isSuperSetOfExpected = (i: string[]) => {
102+
const s = new Set(i)
103+
return expected.every((v) => s.has(v))
104+
}
105+
const isEqualSetToExpected = (i: string[]) => {
106+
const s = new Set(i)
107+
return expected.every((v) => s.has(v)) && i.every((v) => expectedSet.has(v))
108+
}
109+
110+
// Maybe we could abstract these out into some strategy pattern with classes.
111+
// But this list is short and its unclear if we need to expand it further.
112+
// Also, we might replace this with a common implementation amongst the toolkits.
113+
// So... YAGNI
114+
switch (criteria.type) {
115+
case 'OS':
116+
return isExpected(this.context.os)
117+
case 'ComputeEnv':
118+
return isExpected(this.context.computeEnv)
119+
case 'AuthType':
120+
return hasAnyOfExpected(this.context.authTypes)
121+
case 'AuthRegion':
122+
return hasAnyOfExpected(this.context.authRegions)
123+
case 'AuthState':
124+
return hasAnyOfExpected(this.context.authStates)
125+
case 'AuthScopes':
126+
return isEqualSetToExpected(this.context.authScopes)
127+
case 'InstalledExtensions':
128+
return isSuperSetOfExpected(this.context.installedExtensions)
129+
case 'ActiveExtensions':
130+
return isSuperSetOfExpected(this.context.activeExtensions)
131+
default:
132+
throw new Error(`Unknown criteria type: ${criteria.type}`)
133+
}
134+
}
135+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
import * as vscode from 'vscode'
7+
import { EnvType, OperatingSystem } from '../shared/telemetry/util'
8+
9+
/** Types of information that we can use to determine whether to show a notification or not. */
10+
export type Criteria =
11+
| 'OS'
12+
| 'ComputeEnv'
13+
| 'AuthType'
14+
| 'AuthRegion'
15+
| 'AuthState'
16+
| 'AuthScopes'
17+
| 'InstalledExtensions'
18+
| 'ActiveExtensions'
19+
20+
/** Generic condition where the type determines how the values are evaluated. */
21+
export interface CriteriaCondition {
22+
readonly type: Criteria
23+
readonly values: string[]
24+
}
25+
26+
/** One of the subconditions (clauses) must match to be valid. */
27+
export interface OR {
28+
readonly type: 'or'
29+
readonly clauses: (Range | ExactMatch)[]
30+
}
31+
32+
/** Version must be within the bounds to be valid. Missing bound indicates that bound is open-ended. */
33+
export interface Range {
34+
readonly type: 'range'
35+
readonly lowerInclusive?: string // null means "-inf"
36+
readonly upperExclusive?: string // null means "+inf"
37+
}
38+
39+
/** Version must be equal. */
40+
export interface ExactMatch {
41+
readonly type: 'exactMatch'
42+
readonly values: string[]
43+
}
44+
45+
export type ConditionalClause = Range | ExactMatch | OR
46+
47+
/** How to display the notification. */
48+
export interface UIRenderInstructions {
49+
content: {
50+
[`en-US`]: {
51+
title: string
52+
description: string
53+
}
54+
}
55+
// TODO actions
56+
}
57+
58+
/** Condition/criteria section of a notification. */
59+
export interface DisplayIf {
60+
extensionId: string
61+
ideVersion?: ConditionalClause
62+
extensionVersion?: ConditionalClause
63+
additionalCriteria?: CriteriaCondition[]
64+
}
65+
66+
export interface ToolkitNotification {
67+
id: string
68+
displayIf: DisplayIf
69+
uiRenderInstructions: UIRenderInstructions
70+
}
71+
72+
export interface Notifications {
73+
schemaVersion: string
74+
notifications: ToolkitNotification[]
75+
}
76+
77+
export interface RuleContext {
78+
readonly ideVersion: typeof vscode.version
79+
readonly extensionVersion: string
80+
readonly os: OperatingSystem
81+
readonly computeEnv: EnvType
82+
readonly authTypes: string[]
83+
readonly authRegions: string[]
84+
readonly authStates: string[]
85+
readonly authScopes: string[]
86+
readonly installedExtensions: string[]
87+
readonly activeExtensions: string[]
88+
}

packages/core/src/shared/telemetry/util.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ export function getUserAgent(
226226
* NOTES:
227227
* - append `-amzn` for any environment internal to Amazon
228228
*/
229-
type EnvType =
229+
export type EnvType =
230230
| 'cloud9'
231231
| 'cloud9-codecatalyst'
232232
| 'cloudDesktop-amzn'
@@ -322,12 +322,13 @@ export function getOptOutPreference() {
322322
return globals.telemetry.telemetryEnabled ? 'OPTIN' : 'OPTOUT'
323323
}
324324

325+
export type OperatingSystem = 'MAC' | 'WINDOWS' | 'LINUX'
325326
/**
326327
* Useful for populating the sendTelemetryEvent request from codewhisperer's api for publishing custom telemetry events for AB Testing.
327328
*
328329
* Returns one of the enum values of the OperatingSystem model (see SendTelemetryRequest model in the codebase)
329330
*/
330-
export function getOperatingSystem(): 'MAC' | 'WINDOWS' | 'LINUX' {
331+
export function getOperatingSystem(): OperatingSystem {
331332
const osId = os.platform() // 'darwin', 'win32', 'linux', etc.
332333
if (osId === 'darwin') {
333334
return 'MAC'

0 commit comments

Comments
 (0)