Skip to content

Commit

Permalink
Merge master into feature/cwltail
Browse files Browse the repository at this point in the history
  • Loading branch information
aws-toolkit-automation authored Oct 17, 2024
2 parents 6a138d4 + b958880 commit 6a578aa
Show file tree
Hide file tree
Showing 4 changed files with 697 additions and 2 deletions.
135 changes: 135 additions & 0 deletions packages/core/src/notifications/rules.ts
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}`)
}
}
}
88 changes: 88 additions & 0 deletions packages/core/src/notifications/types.ts
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[]
}
5 changes: 3 additions & 2 deletions packages/core/src/shared/telemetry/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ export function getUserAgent(
* NOTES:
* - append `-amzn` for any environment internal to Amazon
*/
type EnvType =
export type EnvType =
| 'cloud9'
| 'cloud9-codecatalyst'
| 'cloudDesktop-amzn'
Expand Down Expand Up @@ -322,12 +322,13 @@ export function getOptOutPreference() {
return globals.telemetry.telemetryEnabled ? 'OPTIN' : 'OPTOUT'
}

export type OperatingSystem = 'MAC' | 'WINDOWS' | 'LINUX'
/**
* Useful for populating the sendTelemetryEvent request from codewhisperer's api for publishing custom telemetry events for AB Testing.
*
* Returns one of the enum values of the OperatingSystem model (see SendTelemetryRequest model in the codebase)
*/
export function getOperatingSystem(): 'MAC' | 'WINDOWS' | 'LINUX' {
export function getOperatingSystem(): OperatingSystem {
const osId = os.platform() // 'darwin', 'win32', 'linux', etc.
if (osId === 'darwin') {
return 'MAC'
Expand Down
Loading

0 comments on commit 6a578aa

Please sign in to comment.