tools/@aws-cdk/spec2cdk/lib/cdk/augmentation-generator.ts (165 lines of code) (raw):
import { Resource, ResourceAugmentation, ResourceMetric, SpecDatabase } from '@aws-cdk/service-spec-types';
import {
$E,
ClassType,
expr,
InterfaceType,
Module,
MonkeyPatchedType,
Splat,
MemberType,
stmt,
Type,
} from '@cdklabs/typewriter';
import { CDK_CLOUDWATCH, CONSTRUCTS } from './cdk';
import { ResourceClass } from './resource-class';
/**
* Generate augmentation methods for the given types
*
* Augmentation consists of two parts:
*
* - Adding method declarations to an interface (IBucket)
* - Adding implementations for those methods to the base class (BucketBase)
*
* The augmentation file must be imported in `index.ts`.
*
* ----------------------------------------------------------
*
* Generates code similar to the following:
*
* ```
* import <Class>Base from './<class>-base';
*
* declare module './<class>-base' {
* interface <IClass> {
* method(...): Type;
* }
* interface <ClassBase> {
* method(...): Type;
* }
* }
*
* <ClassBase>.prototype.<method> = // ...impl...
* ```
*
* This code may not have been factored the best in terms of how it should
* be modeled in typewriter.
*/
export class AugmentationsModule extends Module {
private _hasAugmentations: boolean = false;
/**
* Modules that contain classes that are normally handwritten
*/
public readonly supportModules = new Array<Module>();
constructor(private readonly db: SpecDatabase, serviceName: string, cloudWatchModuleImport?: string) {
super(`${serviceName}.augmentations`);
this.documentation.push(
`Copyright 2012-${new Date().getFullYear()} Amazon.com, Inc. or its affiliates. All Rights Reserved.`,
);
CDK_CLOUDWATCH.import(this, 'cw', {
fromLocation: cloudWatchModuleImport,
});
}
public get hasAugmentations() {
return this._hasAugmentations;
}
public augmentResource(resource: Resource, resourceClass: ResourceClass) {
for (const { entity: aug } of this.db.follow('isAugmented', resource)) {
if (aug.metrics) {
this._hasAugmentations = true;
new ResourceGenerator(resource, resourceClass, aug, this.supportModules).emit(this);
}
}
}
}
class ResourceGenerator {
private readonly interfaceFile: string;
private readonly classFile: string;
private readonly interfaceName: string;
private readonly className: string;
constructor(
private readonly resource: Resource,
resourceClass: ResourceClass,
private readonly aug: ResourceAugmentation,
private readonly supportModules: Module[],
) {
const l2ClassName = resourceClass.name.replace(/^Cfn/, '');
const l2KebabName = l2ClassName.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase();
this.classFile = `./${aug.baseClassFile ?? `${l2KebabName}-base`}`;
this.className = aug.baseClass ?? `${l2ClassName}Base`;
this.interfaceFile = aug.interfaceFile ? `./${aug.interfaceFile}` : this.classFile;
this.interfaceName = aug.interface ?? `I${l2ClassName}`;
}
public emit(into: Module) {
const interfaceModule = new Module(this.interfaceFile);
const classModule = this.classFile === this.interfaceFile ? interfaceModule : new Module(this.classFile);
this.supportModules.push(...new Set([interfaceModule, classModule]));
classModule.importSelective(into, [this.className]);
const externalInterfaceType = new InterfaceType(interfaceModule, { name: this.interfaceName, export: true });
const externalClassType = this.generateClassType(classModule);
this.emitPatches(into, externalInterfaceType);
this.emitPatches(into, externalClassType);
}
/**
* Generate a ClassType representing the L2 class that we're augmenting
*
* We didn't need this class at all, in principle, but if we want to be able to
* generate the code and compile it independent of the human-written code we're
* going to integrate it into later, we need a representation of this class with
* the right attributes so that the compiler can do some sensible type checking.
*
* This class will only be written to disk if that is explicitly requested during
* code generation.
*/
private generateClassType(classModule: Module) {
CONSTRUCTS.importSelective(classModule, ['Construct']);
const type = new ClassType(classModule, {
name: this.className,
export: true,
extends: CONSTRUCTS.Construct,
});
for (const attrName of Object.values(this.aug.metrics?.dimensions ?? {})) {
type.addProperty({
name: attrName,
type: Type.STRING,
immutable: true,
initializer: expr.lit('dummy'),
});
}
return type;
}
/**
* Emit the interface declarations of our mixins
*
* The declarations will be emitted as an interface, and TypeScript will
* combine them with existing declarations, of either an interface or class type.
*/
private emitPatches(into: Module, targetType: MemberType) {
const iface = new MonkeyPatchedType(into, targetType);
this.emitGenericMethod(iface);
for (const metric of this.aug.metrics?.metrics ?? []) {
this.emitSpecificMethod(iface, metric);
}
}
private emitGenericMethod(type: MemberType): void {
const meth = type.addMethod({
name: 'metric',
docs: {
summary: `Return the given named metric for this ${this.resource.name}`,
},
returnType: CDK_CLOUDWATCH.Metric,
});
const metricName = meth.addParameter({ name: 'metricName', type: Type.STRING });
const props = meth.addParameter({ name: 'props', type: CDK_CLOUDWATCH.MetricOptions, optional: true });
const $this = $E(expr.this_());
meth.addBody(
stmt.ret(
new CDK_CLOUDWATCH.Metric(
expr.object(
{
namespace: expr.lit(this.aug.metrics?.namespace),
metricName,
dimensionsMap: expr.object(
Object.entries(this.aug.metrics?.dimensions ?? {}).map(
([name, attrName]) => [name, $this[attrName]] as const,
),
),
},
new Splat(props),
),
).attachTo($this),
),
);
}
private emitSpecificMethod(iface: MemberType, metric: ResourceMetric) {
const meth = iface.addMethod({
name: metricFunctionName(metric),
docs: {
summary: metric.documentation,
remarks: `${metricStatistic(metric)} over 5 minutes`,
},
returnType: CDK_CLOUDWATCH.Metric,
});
const props = meth.addParameter({
name: 'props',
type: CDK_CLOUDWATCH.MetricOptions,
optional: true,
});
const $this = $E(expr.this_());
meth.addBody(
stmt.ret(
$this.metric(
expr.lit(metric.name),
expr.object(
{
statistic: expr.lit(metricStatistic(metric)),
},
new Splat(props),
),
),
),
);
}
}
function metricFunctionName(metric: ResourceMetric): string {
return `metric${metric.name.replace(/[^a-zA-Z0-9]/g, '')}`;
}
function metricStatistic(metric: ResourceMetric): string {
switch (metric.type) {
case 'attrib':
case undefined:
return 'Average';
case 'count':
return 'Sum';
case 'gauge':
return 'Maximum';
}
}