Skip to content

Commit a895ef9

Browse files
committed
chore(spec2cdk): refactor to make it easier to extend
The `spec2cdk` tool was originally written to generate every service submodule insularly. We want to now extend it to emit structures into a new shared submodule. In order to do that, this refactors the generator to have a more shared, mutable object-oriented design which will be easier to share state in. In addition, simplify the API around file name patterns, because they were being overly flexible and annoying to work with for no real benefit.
1 parent 2c73af3 commit a895ef9

File tree

12 files changed

+297
-269
lines changed

12 files changed

+297
-269
lines changed

tools/@aws-cdk/spec2cdk/lib/cdk/ast.ts

Lines changed: 148 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,32 +5,9 @@ import { CannedMetricsModule } from './canned-metrics';
55
import { CDK_CORE, CONSTRUCTS, ModuleImportLocations } from './cdk';
66
import { SelectiveImport } from './relationship-decider';
77
import { ResourceClass } from './resource-class';
8+
import { FilePatternValues, TsFileWriter, substituteFilePattern } from '../util';
89

9-
/**
10-
* A module containing a single resource
11-
*/
12-
export class ResourceModule extends Module {
13-
public constructor(public readonly service: string, public readonly resource: string) {
14-
super(`@aws-cdk/${service}/${resource}-l1`);
15-
}
16-
}
17-
18-
/**
19-
* A module containing a service
20-
*/
21-
export class ServiceModule extends Module {
22-
public constructor(public readonly service: string, public readonly shortName: string) {
23-
super(`@aws-cdk/${service}`);
24-
}
25-
}
26-
27-
export interface AstBuilderProps {
28-
readonly db: SpecDatabase;
29-
/**
30-
* Override the locations modules are imported from
31-
*/
32-
readonly importLocations?: ModuleImportLocations;
33-
10+
export interface AddServiceProps {
3411
/**
3512
* Append a suffix at the end of generated names.
3613
*/
@@ -42,82 +19,126 @@ export interface AstBuilderProps {
4219
* @default - not deprecated
4320
*/
4421
readonly deprecated?: string;
45-
}
4622

47-
export class AstBuilder<T extends Module> {
4823
/**
49-
* Build a module for all resources in a service
24+
* The target resource module we want to generate these resources into
25+
*
26+
* (Practically, only used to render CloudFormation resources into the `core` module.)
5027
*/
51-
public static forService(service: Service, props: AstBuilderProps): AstBuilder<ServiceModule> {
52-
const scope = new ServiceModule(service.name, service.shortName);
53-
const aug = new AugmentationsModule(props.db, service.name, props.importLocations?.cloudwatch);
54-
const metrics = CannedMetricsModule.forService(props.db, service);
28+
readonly destinationModule?: string;
5529

56-
const ast = new AstBuilder(scope, props, aug, metrics);
30+
/**
31+
* Override the locations modules are imported from
32+
*/
33+
readonly importLocations?: ModuleImportLocations;
34+
}
5735

58-
const resources = props.db.follow('hasResource', service);
36+
export interface AstBuilderProps {
37+
readonly db: SpecDatabase;
5938

60-
for (const link of resources) {
61-
ast.addResource(link.entity);
62-
}
63-
ast.renderImports();
64-
return ast;
65-
}
39+
readonly modulesRoot?: string;
40+
}
6641

42+
export interface GenerateFilePatterns {
6743
/**
68-
* Build an module for a single resource
44+
* The pattern used to name resource files.
45+
* @default "%serviceName%/%serviceShortName%.generated.ts"
6946
*/
70-
public static forResource(resource: Resource, props: AstBuilderProps): AstBuilder<ResourceModule> {
71-
const parts = resource.cloudFormationType.toLowerCase().split('::');
72-
const scope = new ResourceModule(parts[1], parts[2]);
73-
const aug = new AugmentationsModule(props.db, parts[1], props.importLocations?.cloudwatch);
74-
const metrics = CannedMetricsModule.forResource(props.db, resource);
75-
76-
const ast = new AstBuilder(scope, props, aug, metrics);
77-
ast.addResource(resource);
78-
ast.renderImports();
47+
readonly resources: string;
7948

80-
return ast;
81-
}
49+
/**
50+
* The pattern used to name augmentations.
51+
* @default "%serviceName%/%serviceShortName%-augmentations.generated.ts"
52+
*/
53+
readonly augmentations: string;
8254

83-
public readonly db: SpecDatabase;
8455
/**
85-
* Map of CloudFormation resource name to generated class name
56+
* The pattern used to name canned metrics.
57+
* @default "%serviceName%/%serviceShortName%-canned-metrics.generated.ts"
8658
*/
87-
public readonly resources: Record<string, string> = {};
88-
private nameSuffix?: string;
89-
private deprecated?: string;
59+
readonly cannedMetrics: string;
60+
}
61+
62+
export const DEFAULT_FILE_PATTERNS: GenerateFilePatterns = {
63+
resources: '%serviceName%/%serviceShortName%.generated.ts',
64+
augmentations: '%serviceName%/%serviceShortName%-augmentations.generated.ts',
65+
cannedMetrics: '%serviceName%/%serviceShortName%-canned-metrics.generated.ts',
66+
};
67+
68+
export class AstBuilder {
69+
public readonly db: SpecDatabase;
70+
9071
public readonly selectiveImports = new Array<SelectiveImport>();
72+
73+
public readonly serviceModules = new Map<string, SubmoduleInfo>();
9174
private readonly modulesRootLocation: string;
9275

93-
protected constructor(
94-
public readonly module: T,
76+
constructor(
9577
props: AstBuilderProps,
96-
public readonly augmentations?: AugmentationsModule,
97-
public readonly cannedMetrics?: CannedMetricsModule,
9878
) {
9979
this.db = props.db;
100-
this.nameSuffix = props.nameSuffix;
101-
this.deprecated = props.deprecated;
102-
this.modulesRootLocation = props.importLocations?.modulesRoot ?? '../..';
103-
104-
CDK_CORE.import(this.module, 'cdk', { fromLocation: props.importLocations?.core });
105-
CONSTRUCTS.import(this.module, 'constructs');
106-
CDK_CORE.helpers.import(this.module, 'cfn_parse', { fromLocation: props.importLocations?.coreHelpers });
107-
CDK_CORE.errors.import(this.module, 'cdk_errors', { fromLocation: props.importLocations?.coreErrors });
80+
this.modulesRootLocation = props.modulesRoot ?? '../..';
10881
}
10982

110-
public addResource(resource: Resource) {
111-
const resourceClass = new ResourceClass(this.module, this.db, resource, {
112-
suffix: this.nameSuffix,
113-
deprecated: this.deprecated,
83+
/**
84+
* Add all resources in a service
85+
*/
86+
public addService(service: Service, props?: AddServiceProps) {
87+
const resources = this.db.follow('hasResource', service);
88+
const submod = this.createSubmodule(service, props?.destinationModule, props?.importLocations);
89+
90+
for (const { entity: resource } of resources) {
91+
this.addResourceToSubmodule(submod, resource, props);
92+
}
93+
94+
this.renderImports(submod);
95+
return submod;
96+
}
97+
98+
/**
99+
* Build an module for a single resource (only used for testing)
100+
*/
101+
public addResource(resource: Resource, props?: AddServiceProps) {
102+
const service = this.db.incoming('hasResource', resource).only().entity;
103+
const submod = this.createSubmodule(service, props?.destinationModule, props?.importLocations);
104+
105+
this.addResourceToSubmodule(submod, resource, props);
106+
107+
this.renderImports(submod);
108+
return submod;
109+
}
110+
111+
public writeAll(writer: TsFileWriter, filePatterns: GenerateFilePatterns) {
112+
for (const mods of this.serviceModules.values()) {
113+
const pattern: FilePatternValues = {
114+
serviceName: mods.service.name,
115+
serviceShortName: mods.service.shortName,
116+
moduleName: mods.moduleName,
117+
};
118+
119+
writer.write(mods.resourceModule, substituteFilePattern(filePatterns.resources, pattern));
120+
121+
if (mods.augmentationsModule.hasAugmentations) {
122+
writer.write(mods.augmentationsModule, substituteFilePattern(filePatterns.augmentations, pattern));
123+
}
124+
125+
if (mods.cannedMetricsModule.hasCannedMetrics) {
126+
writer.write(mods.cannedMetricsModule, substituteFilePattern(filePatterns.cannedMetrics, pattern));
127+
}
128+
}
129+
}
130+
131+
private addResourceToSubmodule(submodule: SubmoduleInfo, resource: Resource, props?: AddServiceProps) {
132+
const resourceClass = new ResourceClass(submodule.resourceModule, this.db, resource, {
133+
suffix: props?.nameSuffix,
134+
deprecated: props?.deprecated,
114135
});
115-
this.resources[resource.cloudFormationType] = resourceClass.spec.name;
136+
submodule.resources[resource.cloudFormationType] = resourceClass.spec.name;
116137

117138
resourceClass.build();
118139

119140
this.addImports(resourceClass);
120-
this.augmentations?.augmentResource(resource, resourceClass);
141+
submodule.augmentationsModule.augmentResource(resource, resourceClass);
121142
}
122143

123144
private addImports(resourceClass: ResourceClass) {
@@ -140,11 +161,61 @@ export class AstBuilder<T extends Module> {
140161
}
141162
}
142163

143-
public renderImports() {
164+
private renderImports(serviceModules: SubmoduleInfo) {
144165
const sortedImports = this.selectiveImports.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
145166
for (const selectiveImport of sortedImports) {
146167
const sourceModule = new Module(selectiveImport.moduleName);
147-
sourceModule.importSelective(this.module, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), { fromLocation: `${this.modulesRootLocation}/${sourceModule.name}` });
168+
sourceModule.importSelective(serviceModules.resourceModule, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), {
169+
fromLocation: `${this.modulesRootLocation}/${sourceModule.name}`,
170+
});
148171
}
149172
}
173+
174+
private createSubmodule(service: Service, targetServiceModule?: string, importLocations?: ModuleImportLocations): SubmoduleInfo {
175+
const moduleName = targetServiceModule ?? service.name;
176+
177+
const mods = this.serviceModules.get(moduleName);
178+
if (mods) {
179+
// eslint-disable-next-line @cdklabs/no-throw-default-error
180+
throw new Error(`A submodule named ${moduleName} was already created`);
181+
}
182+
183+
const resourceModule = new Module(`@aws-cdk/${moduleName}`);
184+
CDK_CORE.import(resourceModule, 'cdk', { fromLocation: importLocations?.core });
185+
CONSTRUCTS.import(resourceModule, 'constructs');
186+
CDK_CORE.helpers.import(resourceModule, 'cfn_parse', { fromLocation: importLocations?.coreHelpers });
187+
CDK_CORE.errors.import(resourceModule, 'cdk_errors', { fromLocation: importLocations?.coreErrors });
188+
189+
const augmentationsModule = new AugmentationsModule(this.db, service.shortName, importLocations?.cloudwatch);
190+
const cannedMetricsModule = CannedMetricsModule.forService(this.db, service);
191+
192+
const ret: SubmoduleInfo = {
193+
service,
194+
moduleName,
195+
resourceModule,
196+
augmentationsModule,
197+
cannedMetricsModule,
198+
resources: {},
199+
};
200+
this.serviceModules.set(moduleName, ret);
201+
return ret;
202+
}
203+
}
204+
205+
export interface SubmoduleInfo {
206+
readonly service: Service;
207+
208+
/**
209+
* The name of the submodule of aws-cdk-lib
210+
*/
211+
readonly moduleName: string;
212+
213+
readonly resourceModule: Module;
214+
readonly augmentationsModule: AugmentationsModule;
215+
readonly cannedMetricsModule: CannedMetricsModule;
216+
217+
/**
218+
* Map of CloudFormation resource name to generated class name
219+
*/
220+
readonly resources: Record<string, string>;
150221
}

tools/@aws-cdk/spec2cdk/lib/cfn2ts/index.ts

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,6 @@ import { log } from '../util';
1010

1111
export * from './types';
1212

13-
interface GenerateOutput {
14-
outputFiles: string[];
15-
resources: Record<string, string>;
16-
}
17-
1813
let serviceCache: Service[];
1914

2015
async function getAllScopes(field: keyof Service = 'name'): Promise<ModuleMapScope[]> {
@@ -30,7 +25,7 @@ export default async function generate(
3025
scopes: string | string[],
3126
outPath: string,
3227
options: CodeGeneratorOptions = {},
33-
): Promise<GenerateOutput> {
28+
): Promise<void> {
3429
const coreImport = options.coreImport ?? 'aws-cdk-lib';
3530
let moduleScopes: ModuleMapScope[] = [];
3631
if (scopes === '*') {
@@ -40,7 +35,7 @@ export default async function generate(
4035
}
4136

4237
log.info(`cfn-resources: ${moduleScopes.map(s => s.namespace).join(', ')}`);
43-
const generated = await generateModules(
38+
await generateModules(
4439
{
4540
'aws-cdk-lib': {
4641
services: options.autoGenerateSuffixes ? computeServiceSuffixes(moduleScopes) : moduleScopes,
@@ -50,9 +45,9 @@ export default async function generate(
5045
outputPath: outPath ?? 'lib',
5146
clearOutput: false,
5247
filePatterns: {
53-
resources: ({ serviceShortName }) => `${serviceShortName}.generated.ts`,
54-
augmentations: ({ serviceShortName }) => `${serviceShortName}-augmentations.generated.ts`,
55-
cannedMetrics: ({ serviceShortName }) => `${serviceShortName}-canned-metrics.generated.ts`,
48+
resources: '%serviceShortName.generated.ts',
49+
augmentations: '%serviceShortName%-augmentations.generated.ts',
50+
cannedMetrics: '%serviceShortName%-canned-metrics.generated.ts',
5651
},
5752
importLocations: {
5853
core: coreImport,
@@ -61,8 +56,6 @@ export default async function generate(
6156
},
6257
},
6358
);
64-
65-
return generated;
6659
}
6760

6861
/**
@@ -163,9 +156,9 @@ export async function generateAll(
163156
outputPath: outPath,
164157
clearOutput: false,
165158
filePatterns: {
166-
resources: ({ moduleName: m, serviceShortName: s }) => `${m}/lib/${s}.generated.ts`,
167-
augmentations: ({ moduleName: m, serviceShortName: s }) => `${m}/lib/${s}-augmentations.generated.ts`,
168-
cannedMetrics: ({ moduleName: m, serviceShortName: s }) => `${m}/lib/${s}-canned-metrics.generated.ts`,
159+
resources: '%moduleName%/lib/%serviceShortName%.generated.ts',
160+
augmentations: '%moduleName%/lib/%serviceShortName%-augmentations.generated.ts',
161+
cannedMetrics: '%moduleName%/lib/%serviceShortName%-canned-metrics.generated.ts',
169162
},
170163
importLocations: {
171164
core: options.coreImport,

0 commit comments

Comments
 (0)