Skip to content

Commit 50d3abe

Browse files
committed
chore(spec2cdk): refactor code generation with LibraryBuilder abstraction
Introduces a new LibraryBuilder base class to abstract the code generation process for CDK libraries. This refactoring separates concerns between the general library building logic and aws-cdk-lib-specific implementation. Key changes: - Create LibraryBuilder base class with generic service submodule support - Extract AwsCdkLibBuilder as a specialized implementation - Introduce ServiceSubmodule abstraction for better modularity - Move selective imports logic to service-submodule - Update TypeConverter to support partial type definitions for mixins - Adjust import paths and file patterns configuration
1 parent af61744 commit 50d3abe

18 files changed

+728
-580
lines changed

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

Lines changed: 0 additions & 489 deletions
This file was deleted.
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
/* eslint-disable @cdklabs/no-throw-default-error */
2+
import { Resource, Service } from '@aws-cdk/service-spec-types';
3+
import { Module, stmt } from '@cdklabs/typewriter';
4+
import { AugmentationsModule } from './augmentation-generator';
5+
import { CannedMetricsModule } from './canned-metrics';
6+
import { CDK_CORE, CDK_INTERFACES_ENVIRONMENT_AWARE, CONSTRUCTS } from './cdk';
7+
import { AddServiceProps, LibraryBuilder, LibraryBuilderProps } from './library-builder';
8+
import { ResourceClass } from './resource-class';
9+
import { LocatedModule, relativeImportPath, BaseServiceSubmodule } from './service-submodule';
10+
import { submoduleSymbolFromName } from '../naming';
11+
12+
class AwsCdkLibServiceSubmodule extends BaseServiceSubmodule {
13+
public readonly resourcesMod: LocatedModule<Module>;
14+
public readonly augmentations: LocatedModule<AugmentationsModule>;
15+
public readonly cannedMetrics: LocatedModule<CannedMetricsModule>;
16+
public readonly interfaces: LocatedModule<Module>;
17+
public readonly didCreateInterfaceModule: boolean;
18+
19+
public constructor(props: {
20+
readonly submoduleName: string;
21+
readonly service: Service;
22+
readonly resourcesMod: LocatedModule<Module>;
23+
readonly augmentations: LocatedModule<AugmentationsModule>;
24+
readonly cannedMetrics: LocatedModule<CannedMetricsModule>;
25+
readonly interfaces: LocatedModule<Module>;
26+
readonly didCreateInterfaceModule: boolean;
27+
}) {
28+
super(props);
29+
this.resourcesMod = props.resourcesMod;
30+
this.augmentations = props.augmentations;
31+
this.cannedMetrics = props.cannedMetrics;
32+
this.interfaces = props.interfaces;
33+
this.didCreateInterfaceModule = props.didCreateInterfaceModule;
34+
35+
this.registerModule(this.resourcesMod);
36+
this.registerModule(this.cannedMetrics);
37+
this.registerModule(this.augmentations);
38+
this.registerModule(this.interfaces);
39+
}
40+
}
41+
42+
export interface AwsCdkLibFilePatterns {
43+
/**
44+
* The pattern used to name resource files.
45+
*/
46+
readonly resources: string;
47+
48+
/**
49+
* The pattern used to name augmentations.
50+
*/
51+
readonly augmentations: string;
52+
53+
/**
54+
* The pattern used to name canned metrics.
55+
*/
56+
readonly cannedMetrics: string;
57+
58+
/**
59+
* The pattern used to name the interfaces entry point file
60+
*/
61+
readonly interfacesEntry: string;
62+
63+
/**
64+
* The pattern used to name the interfaces file
65+
*/
66+
readonly interfaces: string;
67+
}
68+
69+
export interface AwsCdkLibBuilderProps extends LibraryBuilderProps{
70+
/**
71+
* The file patterns used to generate the files.
72+
*
73+
* @default - default patterns
74+
*/
75+
readonly filePatterns?: Partial<AwsCdkLibFilePatterns>;
76+
77+
/**
78+
* Whether we are generating code INSIDE `aws-cdk-lib` or outside it
79+
*
80+
* This affects the generated import paths.
81+
*/
82+
readonly inCdkLib?: boolean;
83+
}
84+
85+
export const DEFAULT_FILE_PATTERNS: AwsCdkLibFilePatterns = {
86+
resources: '%moduleName%/%serviceShortName%.generated.ts',
87+
augmentations: '%moduleName%/%serviceShortName%-augmentations.generated.ts',
88+
cannedMetrics: '%moduleName%/%serviceShortName%-canned-metrics.generated.ts',
89+
interfacesEntry: 'interfaces/index.generated.ts',
90+
interfaces: 'interfaces/generated/%serviceName%-interfaces.generated.ts',
91+
};
92+
93+
/**
94+
* The library builder for `aws-cdk-lib`.
95+
*
96+
* Contains the spec
97+
*/
98+
export class AwsCdkLibBuilder extends LibraryBuilder<AwsCdkLibServiceSubmodule> {
99+
private readonly inCdkLib: boolean;
100+
private readonly filePatterns: AwsCdkLibFilePatterns;
101+
private readonly interfacesEntry: LocatedModule<Module>;
102+
103+
public constructor(props: AwsCdkLibBuilderProps) {
104+
super(props);
105+
this.filePatterns = {
106+
...DEFAULT_FILE_PATTERNS,
107+
...noUndefined(props?.filePatterns ?? {}),
108+
};
109+
this.inCdkLib = props.inCdkLib ?? false;
110+
111+
if (this.filePatterns.interfacesEntry.includes('%')) {
112+
throw new Error(`interfacesEntry may not contain placeholders, got: ${this.filePatterns.interfacesEntry}`);
113+
}
114+
115+
this.interfacesEntry = this.rememberModule({
116+
module: new Module('interfaces/index'),
117+
filePath: this.filePatterns.interfacesEntry,
118+
});
119+
}
120+
121+
protected createServiceSubmodule(service: Service, submoduleName: string): AwsCdkLibServiceSubmodule {
122+
const resourcesMod = this.rememberModule(this.createResourceModule(submoduleName, service));
123+
const augmentations = this.rememberModule(this.createAugmentationsModule(submoduleName, service));
124+
const cannedMetrics = this.rememberModule(this.createCannedMetricsModule(submoduleName, service));
125+
const [interfaces, didCreateInterfaceModule] = this.obtainInterfaceModule(service);
126+
127+
const createdSubmod: AwsCdkLibServiceSubmodule = new AwsCdkLibServiceSubmodule({
128+
submoduleName,
129+
service,
130+
resourcesMod,
131+
augmentations,
132+
cannedMetrics,
133+
interfaces,
134+
didCreateInterfaceModule,
135+
});
136+
137+
return createdSubmod;
138+
}
139+
140+
protected addResourceToSubmodule(submodule: AwsCdkLibServiceSubmodule, resource: Resource, props?: AddServiceProps) {
141+
const resourceModule = submodule.resourcesMod.module;
142+
143+
const resourceClass = new ResourceClass(resourceModule, this.db, resource, {
144+
suffix: props?.nameSuffix,
145+
deprecated: props?.deprecated,
146+
interfacesModule: submodule.interfaces ? {
147+
module: submodule.interfaces?.module,
148+
importLocation: relativeImportPath(submodule.resourcesMod, submodule.interfaces),
149+
} : undefined,
150+
});
151+
152+
resourceClass.build();
153+
154+
submodule.registerResource(resource.cloudFormationType, resourceClass);
155+
submodule.registerSelectiveImports(...resourceClass.imports);
156+
submodule.augmentations?.module.augmentResource(resource, resourceClass);
157+
158+
for (const selectiveImport of submodule.imports) {
159+
const sourceModule = new Module(selectiveImport.moduleName);
160+
sourceModule.importSelective(submodule.resourcesMod.module, selectiveImport.types.map((t) => `${t.originalType} as ${t.aliasedType}`), {
161+
fromLocation: relativeImportPath(submodule.resourcesMod.filePath, sourceModule.name),
162+
});
163+
}
164+
}
165+
166+
private createResourceModule(moduleName: string, service: Service): LocatedModule<Module> {
167+
const filePath = this.pathsFor(moduleName, service).resources;
168+
const imports = this.resolveImportPaths(filePath);
169+
170+
const module = new Module(`@aws-cdk/${moduleName}/${service.name}`);
171+
172+
CDK_CORE.import(module, 'cdk', { fromLocation: imports.core });
173+
CONSTRUCTS.import(module, 'constructs');
174+
CDK_CORE.helpers.import(module, 'cfn_parse', { fromLocation: imports.coreHelpers });
175+
CDK_CORE.errors.import(module, 'cdk_errors', { fromLocation: imports.coreErrors });
176+
177+
return { module, filePath };
178+
}
179+
180+
private createAugmentationsModule(moduleName: string, service: Service): LocatedModule<AugmentationsModule> {
181+
const filePath = this.pathsFor(moduleName, service).augmentations;
182+
const imports = this.resolveImportPaths(filePath);
183+
return {
184+
module: new AugmentationsModule(this.db, service.shortName, imports.cloudwatch),
185+
filePath,
186+
};
187+
}
188+
189+
private createCannedMetricsModule(moduleName: string, service: Service): LocatedModule<CannedMetricsModule> {
190+
const filePath = this.pathsFor(moduleName, service).cannedMetrics;
191+
return {
192+
module: CannedMetricsModule.forService(this.db, service),
193+
filePath,
194+
};
195+
}
196+
197+
/**
198+
* Create or find the module where we should add the interfaces for these resources
199+
*
200+
* Complicated by the fact that we generate classes for some services in multiple places, but we should only generate the interfaces once.
201+
*/
202+
private obtainInterfaceModule(service: Service): [LocatedModule<Module>, boolean] {
203+
const filePath = this.pathsFor('$UNUSED$', service).interfaces;
204+
205+
return this.modules.has(filePath)
206+
? [{ module: this.modules.get(filePath)!, filePath }, false]
207+
: [this.rememberModule(this.createInterfaceModule(service)), true];
208+
}
209+
210+
private createInterfaceModule(service: Service): LocatedModule<Module> {
211+
const filePath = this.pathsFor('$UNUSED$', service).interfaces;
212+
const imports = this.resolveImportPaths(filePath);
213+
214+
const module = new Module(`@aws-cdk/interfaces/${service.name}`);
215+
CDK_INTERFACES_ENVIRONMENT_AWARE.importSelective(module, ['IEnvironmentAware'], {
216+
fromLocation: imports.interfacesEnvironmentAware,
217+
});
218+
CONSTRUCTS.import(module, 'constructs');
219+
220+
return { module, filePath };
221+
}
222+
223+
/**
224+
* Do whatever we need to do after a service has been rendered to a submodule
225+
*
226+
* (Mostly: create additional files that import generated files)
227+
*/
228+
protected postprocessSubmodule(submodule: AwsCdkLibServiceSubmodule) {
229+
super.postprocessSubmodule(submodule);
230+
231+
// Add an import for the interfaces file to the entry point file (make sure not to do it twice)
232+
if (!submodule.interfaces?.module.isEmpty() && this.interfacesEntry && submodule.didCreateInterfaceModule) {
233+
const exportName = submoduleSymbolFromName(submodule.service.name);
234+
const importLocation = relativeImportPath(this.interfacesEntry, submodule.interfaces);
235+
236+
this.interfacesEntry.module.addInitialization(stmt.directCode(
237+
`export * as ${exportName} from '${importLocation}'`,
238+
));
239+
}
240+
}
241+
242+
private resolveImportPaths(sourceModule: string): ImportPaths {
243+
if (!this.inCdkLib) {
244+
return {
245+
core: 'aws-cdk-lib/core',
246+
interfacesEnvironmentAware: 'aws-cdk-lib/interfaces',
247+
coreHelpers: 'aws-cdk-lib/core/lib/helpers-internal',
248+
coreErrors: 'aws-cdk-lib/core/lib/errors',
249+
cloudwatch: 'aws-cdk-lib/aws-cloudwatch',
250+
};
251+
}
252+
253+
return {
254+
core: relativeImportPath(sourceModule, 'core/lib'),
255+
interfacesEnvironmentAware: relativeImportPath(sourceModule, 'interfaces/environment-aware'),
256+
coreHelpers: relativeImportPath(sourceModule, 'core/lib/helpers-internal'),
257+
coreErrors: relativeImportPath(sourceModule, 'core/lib/errors'),
258+
cloudwatch: relativeImportPath(sourceModule, 'aws-cloudwatch'),
259+
};
260+
}
261+
262+
private pathsFor(submoduleName: string, service: Service): Record<keyof AwsCdkLibFilePatterns, string> {
263+
return Object.fromEntries(Object.entries(this.filePatterns)
264+
.map(([name, pattern]) => [name, this.pathFor(pattern, submoduleName, service)] as const)) as any;
265+
}
266+
}
267+
268+
function noUndefined<A extends object>(x: A | undefined): A | undefined {
269+
if (!x) {
270+
return undefined;
271+
}
272+
return Object.fromEntries(Object.entries(x).filter(([, v]) => v !== undefined)) as any;
273+
}
274+
275+
export interface ImportPaths {
276+
/**
277+
* The import name used import the core module
278+
*/
279+
readonly core: string;
280+
281+
/**
282+
* The import name used import a specific interface from the `interfaces` module
283+
*
284+
* Not the entire module but a specific file, so that if we're codegenning inside `aws-cdk-lib`
285+
* we can pinpoint the exact file we need to load.
286+
*/
287+
readonly interfacesEnvironmentAware: string;
288+
289+
/**
290+
* The import name used to import core helpers module
291+
*/
292+
readonly coreHelpers: string;
293+
294+
/**
295+
* The import name used to import core errors module
296+
*/
297+
readonly coreErrors: string;
298+
299+
/**
300+
* The import name used to import the CloudWatch module
301+
*/
302+
readonly cloudwatch: string;
303+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ export class CdkCloudWatch extends ExternalModule {
104104
}
105105

106106
export const CDK_INTERFACES_ENVIRONMENT_AWARE = new Interfaces('aws-cdk-lib/interfaces/environment-aware');
107-
export const CDK_CORE = new CdkCore('aws-cdk-lib');
107+
export const CDK_CORE = new CdkCore('aws-cdk-lib/core');
108108
export const CDK_CLOUDWATCH = new CdkCloudWatch('aws-cdk-lib/aws-cloudwatch');
109109
export const CONSTRUCTS = new Constructs();
110110

0 commit comments

Comments
 (0)