Skip to content

Commit

Permalink
refactor: introduce ClassToInterface, infer interfaces from impl #1842
Browse files Browse the repository at this point in the history
SDK client interfaces were 1:1 with implementation, so let's just create
interfaces from the implementation rather than keep track of two separate
files.
- Does not affect anything outside the shared/clients/ directory: the
  interfaces still exist, we just don't need to change them separately anymore.
  Bonus: now you can go straight to the implementation via "Go to definition".
- Introduced the `tsUtils:ClassToInterface` type. Because the result now exactly
  matches the class, this discovered minor differences in the tests, such as
  marking types unioned with undefined which is not the same as an optional
  type.

Potential followup work:

The opaque client wrappers still exist of course, but now making changes should
now be slightly less obnoxious. It may be worth just setting the base
prototypes of the default clients to pass-through to the wrapped SDK client.
Interfaces can just be created from types to mix-in the wrapped functions. So
the wrapped clients can still live separately, but callers can access the
underlying client in an intuitive way. In other words, the caller would not
know that the client is wrapped.

Also, we have no tests for our client wrappers other than S3 and ECS. We should
look towards creating those before further changes. At least for the functions
that aren't just transforming the calls to promises (this could just be made
default by the prototype until we move to v3)
  • Loading branch information
JadenSimon authored Jul 2, 2021
1 parent 461c3bd commit 9dbb898
Show file tree
Hide file tree
Showing 47 changed files with 1,506 additions and 1,870 deletions.
2 changes: 2 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ CHANGELOG.md
src/shared/telemetry/service-2.json
.changes
media/libs/vue.min.js
docs/**
src/testFixtures/**
4 changes: 2 additions & 2 deletions README.quickstart.vscode.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ palette_, available by selecting _View > Command Palette_ or by typing

| AWS Command | Description |
| :--------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `AWS: About Toolkit` | Displays information about the AWS Toolkit. |
| `AWS: About Toolkit` | Displays information about the AWS Toolkit. |
| `AWS: Add SAM Debug Configuration` | Creates an `aws-sam` Debug Configuration from a function in the current source file |
| `AWS: Connect to AWS` | Connects the Toolkit to an AWS account. For more information, see [Connecting to AWS](https://docs.aws.amazon.com/console/toolkit-for-vscode/connect) in the user guide. |
| `AWS: Copy Log Stream Name` | Copies the name of the active CloudWatch Log Stream |
Expand All @@ -228,7 +228,7 @@ palette_, available by selecting _View > Command Palette_ or by typing
| `AWS: Sign out` | Disconnects the Toolkit from the currently-connected AWS account. |
| `AWS: Submit Quick Feedback...` | Submit a private, one-way message and sentiment to the AWS Toolkit dev team. For larger issues that warrant conversations or bugfixes, please submit an issue in Github with the **AWS: Create a New Issue on Github** command. |
| `AWS: Toggle SAM hints in source files` | Toggles AWS SAM-related Codelenses in source files |
| `AWS: View Toolkit Logs` | Displays log files that contain general Toolkit diagnostic information. |
| `AWS: View Toolkit Logs` | Displays log files that contain general Toolkit diagnostic information. |
| `AWS: View Quick Start` | Open this quick-start guide. |
| `AWS: View CDK Documentation` | Opens the [user guide](https://docs.aws.amazon.com/console/toolkit-for-vscode/aws-cdk-apps) for the CDK portion of the Toolkit. |
| `AWS: View Toolkit Documentation` | Opens the [user guide](https://docs.aws.amazon.com/console/toolkit-for-vscode/welcome) for the Toolkit. |
Expand Down
3 changes: 3 additions & 0 deletions docs/CODE_GUIDELINES.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ that is a net cost.
- Do not habitually define concepts one-per-file. Related classes, interfaces,
and symbols should live in the same module unless there is an explicit,
conscious motivation for separating them.
- Use the `ClassToInterfaceType` type alias when an interface that directly
corresponds to a class is needed. This keeps things organized and reduces
the number of changes required when the implementation is modified.
- What is a "unit test"? https://www.youtube.com/watch?v=EZ05e7EMOLM
- Unit is not "a single class", it's a piece of the software with a clear
boundary. It might be a whole microservice, or a chunk of a monolith that
Expand Down
2 changes: 1 addition & 1 deletion package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -421,7 +421,7 @@
"AWS.s3.downloadFile.error.general": "Failed to download file {0}",
"AWS.s3.uploadFile.progressTitle": "Uploading {0}...",
"AWS.s3.uploadFile.openButton": "Upload",
"AWS.s3.uploadFile.startUpload" : "Uploading file from {0} to {1}",
"AWS.s3.uploadFile.startUpload": "Uploading file from {0} to {1}",
"AWS.s3.uploadFile.success": "Successfully uploaded file {0} to {1}",
"AWS.s3.uploadFile.error.general": "Failed to upload file {0}",
"AWS.s3.validateBucketName.error.invalidCharacters": "Bucket name must only contain lowercase letters, numbers, hyphens, and periods",
Expand Down
2 changes: 1 addition & 1 deletion src/credentials/providers/ec2CredentialsProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { Credentials, EC2MetadataCredentials } from 'aws-sdk'
import { DefaultEc2MetadataClient } from '../../shared/clients/defaultEc2MetadataClient'
import { DefaultEc2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { Ec2MetadataClient } from '../../shared/clients/ec2MetadataClient'
import { getLogger } from '../../shared/logger'
import { CredentialType } from '../../shared/telemetry/telemetry.gen'
Expand Down
2 changes: 1 addition & 1 deletion src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import { activate as activateSchemas } from './eventSchemas/activation'
import { activate as activateLambda } from './lambda/activation'
import { DefaultAWSClientBuilder } from './shared/awsClientBuilder'
import { AwsContextTreeCollection } from './shared/awsContextTreeCollection'
import { DefaultToolkitClientBuilder } from './shared/clients/defaultToolkitClientBuilder'
import { DefaultToolkitClientBuilder } from './shared/clients/toolkitClientBuilder'
import { activate as activateCloudFormationTemplateRegistry } from './shared/cloudformation/activation'
import {
documentationUrl,
Expand Down
4 changes: 2 additions & 2 deletions src/lambda/commands/createNewSamApp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -360,8 +360,8 @@ export async function getProjectUri(
let file: string
let cfnTemplatePath: string
for (const f of files) {
file = f
cfnTemplatePath = path.resolve(config.location.fsPath, config.name, file)
file = f
cfnTemplatePath = path.resolve(config.location.fsPath, config.name, file)
if (await fileExists(cfnTemplatePath)) {
return vscode.Uri.file(cfnTemplatePath)
}
Expand Down
26 changes: 21 additions & 5 deletions src/lambda/models/samTemplates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,27 @@ export type SamTemplate = string
* Lazy load strings for SAM template quick picks
* Need to be lazyloaded as `getIdeProperties` requires IDE activation for Cloud9
*/
export function lazyLoadSamTemplateStrings(): void{
helloWorldTemplate = localize('AWS.samcli.initWizard.template.helloWorld.name', '{0} SAM Hello World', getIdeProperties().company)
eventBridgeHelloWorldTemplate = localize('AWS.samcli.initWizard.template.helloWorld.name', '{0} SAM EventBridge Hello World', getIdeProperties().company)
eventBridgeStarterAppTemplate = localize('AWS.samcli.initWizard.template.helloWorld.name', '{0} SAM EventBridge App from Scratch', getIdeProperties().company)
stepFunctionsSampleApp = localize('AWS.samcli.initWizard.template.helloWorld.name', '{0} Step Functions Sample App', getIdeProperties().company)
export function lazyLoadSamTemplateStrings(): void {
helloWorldTemplate = localize(
'AWS.samcli.initWizard.template.helloWorld.name',
'{0} SAM Hello World',
getIdeProperties().company
)
eventBridgeHelloWorldTemplate = localize(
'AWS.samcli.initWizard.template.helloWorld.name',
'{0} SAM EventBridge Hello World',
getIdeProperties().company
)
eventBridgeStarterAppTemplate = localize(
'AWS.samcli.initWizard.template.helloWorld.name',
'{0} SAM EventBridge App from Scratch',
getIdeProperties().company
)
stepFunctionsSampleApp = localize(
'AWS.samcli.initWizard.template.helloWorld.name',
'{0} Step Functions Sample App',
getIdeProperties().company
)
}

export function getSamTemplateWizardOption(
Expand Down
76 changes: 68 additions & 8 deletions src/shared/clients/apiGatewayClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,82 @@
* SPDX-License-Identifier: Apache-2.0
*/

import { RestApi, Resource, TestInvokeMethodResponse, Stages } from 'aws-sdk/clients/apigateway'
import { APIGateway } from 'aws-sdk'
import { ext } from '../extensionGlobals'
import '../utilities/asyncIteratorShim'
import { RestApi, Stages } from 'aws-sdk/clients/apigateway'
import { ClassToInterfaceType } from '../utilities/tsUtils'

export interface ApiGatewayClient {
readonly regionCode: string
export type ApiGatewayClient = ClassToInterfaceType<DefaultApiGatewayClient>
export class DefaultApiGatewayClient {
public constructor(public readonly regionCode: string) {}

getResourcesForApi(apiId: string): AsyncIterableIterator<Resource>
public async *getResourcesForApi(apiId: string): AsyncIterableIterator<APIGateway.Resource> {
const client = await this.createSdkClient()

getStages(apiId: string): Promise<Stages>
const request: APIGateway.GetResourcesRequest = {
restApiId: apiId,
}

listApis(): AsyncIterableIterator<RestApi>
do {
const response: APIGateway.Resources = await client.getResources(request).promise()

testInvokeMethod(
if (response.items !== undefined && response.items.length > 0) {
yield* response.items
}

request.position = response.position
} while (request.position !== undefined)
}

public async getStages(apiId: string): Promise<Stages> {
const client = await this.createSdkClient()

const request: APIGateway.GetResourcesRequest = {
restApiId: apiId,
}

return client.getStages(request).promise()
}

public async *listApis(): AsyncIterableIterator<RestApi> {
const client = await this.createSdkClient()

const request: APIGateway.GetRestApisRequest = {}

do {
const response: APIGateway.RestApis = await client.getRestApis(request).promise()

if (response.items !== undefined && response.items.length > 0) {
yield* response.items
}

request.position = response.position
} while (request.position !== undefined)
}

public async testInvokeMethod(
apiId: string,
resourceId: string,
method: string,
body: string,
pathWithQueryString: string | undefined
): Promise<TestInvokeMethodResponse>
): Promise<APIGateway.TestInvokeMethodResponse> {
const client = await this.createSdkClient()
const request: APIGateway.TestInvokeMethodRequest = {
restApiId: apiId,
resourceId: resourceId,
httpMethod: method,
body: body,
}
if (pathWithQueryString) {
request.pathWithQueryString = pathWithQueryString
}

return await client.testInvokeMethod(request).promise()
}

private async createSdkClient(): Promise<APIGateway> {
return await ext.sdkClientBuilder.createAwsService(APIGateway, undefined, this.regionCode)
}
}
52 changes: 47 additions & 5 deletions src/shared/clients/cloudFormationClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,55 @@
*/

import { CloudFormation } from 'aws-sdk'
import { ext } from '../extensionGlobals'
import '../utilities/asyncIteratorShim'
import { ClassToInterfaceType } from '../utilities/tsUtils'

export interface CloudFormationClient {
readonly regionCode: string
export type CloudFormationClient = ClassToInterfaceType<DefaultCloudFormationClient>
export class DefaultCloudFormationClient {
public constructor(public readonly regionCode: string) {}

deleteStack(name: string): Promise<void>
public async deleteStack(name: string): Promise<void> {
const client = await this.createSdkClient()

listStacks(statusFilter?: string[]): AsyncIterableIterator<CloudFormation.StackSummary>
await client
.deleteStack({
StackName: name,
})
.promise()
}

describeStackResources(name: string): Promise<CloudFormation.DescribeStackResourcesOutput>
public async *listStacks(
statusFilter: string[] = ['CREATE_COMPLETE', 'UPDATE_COMPLETE']
): AsyncIterableIterator<CloudFormation.StackSummary> {
const client = await this.createSdkClient()

const request: CloudFormation.ListStacksInput = {
StackStatusFilter: statusFilter,
}

do {
const response: CloudFormation.ListStacksOutput = await client.listStacks(request).promise()

if (response.StackSummaries) {
yield* response.StackSummaries
}

request.NextToken = response.NextToken
} while (request.NextToken)
}

public async describeStackResources(name: string): Promise<CloudFormation.DescribeStackResourcesOutput> {
const client = await this.createSdkClient()

return await client
.describeStackResources({
StackName: name,
})
.promise()
}

private async createSdkClient(): Promise<CloudFormation> {
return await ext.sdkClientBuilder.createAwsService(CloudFormation, undefined, this.regionCode)
}
}
48 changes: 41 additions & 7 deletions src/shared/clients/cloudWatchLogsClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,49 @@
*/

import { CloudWatchLogs } from 'aws-sdk'
import { ext } from '../extensionGlobals'
import { ClassToInterfaceType } from '../utilities/tsUtils'

export interface CloudWatchLogsClient {
readonly regionCode: string
export type CloudWatchLogsClient = ClassToInterfaceType<DefaultCloudWatchLogsClient>
export class DefaultCloudWatchLogsClient {
public constructor(public readonly regionCode: string) {}

describeLogGroups(): AsyncIterableIterator<CloudWatchLogs.LogGroup>
public async *describeLogGroups(): AsyncIterableIterator<CloudWatchLogs.LogGroup> {
const sdkClient = await this.createSdkClient()
const request: CloudWatchLogs.DescribeLogGroupsRequest = {}
do {
const response = await this.invokeDescribeLogGroups(request, sdkClient)
if (response.logGroups) {
yield* response.logGroups
}
request.nextToken = response.nextToken
} while (request.nextToken)
}

getLogEvents(request: CloudWatchLogs.GetLogEventsRequest): Promise<CloudWatchLogs.GetLogEventsResponse>

describeLogStreams(
public async describeLogStreams(
request: CloudWatchLogs.DescribeLogStreamsRequest
): Promise<CloudWatchLogs.DescribeLogStreamsResponse>
): Promise<CloudWatchLogs.DescribeLogStreamsResponse> {
const sdkClient = await this.createSdkClient()

return sdkClient.describeLogStreams(request).promise()
}

public async getLogEvents(
request: CloudWatchLogs.GetLogEventsRequest
): Promise<CloudWatchLogs.GetLogEventsResponse> {
const sdkClient = await this.createSdkClient()

return sdkClient.getLogEvents(request).promise()
}

protected async invokeDescribeLogGroups(
request: CloudWatchLogs.DescribeLogGroupsRequest,
sdkClient: CloudWatchLogs
): Promise<CloudWatchLogs.DescribeLogGroupsResponse> {
return sdkClient.describeLogGroups(request).promise()
}

protected async createSdkClient(): Promise<CloudWatchLogs> {
return await ext.sdkClientBuilder.createAwsService(CloudWatchLogs, undefined, this.regionCode)
}
}
Loading

0 comments on commit 9dbb898

Please sign in to comment.