Skip to content

Commit 7ec3cc9

Browse files
author
Leon Michalski
committed
Init
1 parent 8638a78 commit 7ec3cc9

File tree

13 files changed

+2818
-158
lines changed

13 files changed

+2818
-158
lines changed

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

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { Module } from '@cdklabs/typewriter';
33
import { AugmentationsModule } from './augmentation-generator';
44
import { CannedMetricsModule } from './canned-metrics';
55
import { CDK_CORE, CONSTRUCTS, ModuleImportLocations } from './cdk';
6+
import { SelectiveImport } from './relationship-decider';
67
import { ResourceClass } from './resource-class';
78

89
/**
@@ -59,7 +60,7 @@ export class AstBuilder<T extends Module> {
5960
for (const link of resources) {
6061
ast.addResource(link.entity);
6162
}
62-
63+
ast.renderImports();
6364
return ast;
6465
}
6566

@@ -74,6 +75,7 @@ export class AstBuilder<T extends Module> {
7475

7576
const ast = new AstBuilder(scope, props, aug, metrics);
7677
ast.addResource(resource);
78+
ast.renderImports();
7779

7880
return ast;
7981
}
@@ -85,6 +87,8 @@ export class AstBuilder<T extends Module> {
8587
public readonly resources: Record<string, string> = {};
8688
private nameSuffix?: string;
8789
private deprecated?: string;
90+
public readonly selectiveImports = new Array<SelectiveImport>();
91+
private readonly modulesRootLocation: string;
8892

8993
protected constructor(
9094
public readonly module: T,
@@ -95,6 +99,7 @@ export class AstBuilder<T extends Module> {
9599
this.db = props.db;
96100
this.nameSuffix = props.nameSuffix;
97101
this.deprecated = props.deprecated;
102+
this.modulesRootLocation = props.importLocations?.modulesRoot ?? '../..';
98103

99104
CDK_CORE.import(this.module, 'cdk', { fromLocation: props.importLocations?.core });
100105
CONSTRUCTS.import(this.module, 'constructs');
@@ -111,6 +116,35 @@ export class AstBuilder<T extends Module> {
111116

112117
resourceClass.build();
113118

119+
this.addImports(resourceClass);
114120
this.augmentations?.augmentResource(resource, resourceClass);
115121
}
122+
123+
private addImports(resourceClass: ResourceClass) {
124+
for (const selectiveImport of resourceClass.imports) {
125+
const existingModuleImport = this.selectiveImports.find(
126+
(imp) => imp.moduleName === selectiveImport.moduleName,
127+
);
128+
if (!existingModuleImport) {
129+
this.selectiveImports.push(selectiveImport);
130+
} else {
131+
// We need to avoid importing the same reference multiple times
132+
for (const type of selectiveImport.types) {
133+
if (!existingModuleImport.types.find((t) =>
134+
t.originalType === type.originalType && t.aliasedType === type.aliasedType,
135+
)) {
136+
existingModuleImport.types.push(type);
137+
}
138+
}
139+
}
140+
}
141+
}
142+
143+
public renderImports() {
144+
const sortedImports = this.selectiveImports.sort((a, b) => a.moduleName.localeCompare(b.moduleName));
145+
for (const selectiveImport of sortedImports) {
146+
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}` });
148+
}
149+
}
116150
}

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ export interface ModuleImportLocations {
2222
* @default 'aws-cdk-lib/aws-cloudwatch'
2323
*/
2424
readonly cloudwatch?: string;
25+
/**
26+
* The root location of all the modules
27+
* @default '../..'
28+
*/
29+
readonly modulesRoot?: string;
2530
}
2631

2732
export class CdkCore extends ExternalModule {
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import { Resource } from '@aws-cdk/service-spec-types';
2+
import { $E, expr, Expression, PropertySpec, Type } from '@cdklabs/typewriter';
3+
import { attributePropertyName, referencePropertyName } from '../naming';
4+
import { CDK_CORE } from './cdk';
5+
6+
export interface ReferenceProp {
7+
readonly declaration: PropertySpec;
8+
readonly cfnValue: Expression;
9+
}
10+
11+
// Convenience typewriter builder
12+
const $this = $E(expr.this_());
13+
14+
export function getReferenceProps(resource: Resource): ReferenceProp[] {
15+
const referenceProps = [];
16+
// Primary identifier. We assume all parts are strings.
17+
const primaryIdentifier = resource.primaryIdentifier ?? [];
18+
if (primaryIdentifier.length === 1) {
19+
referenceProps.push({
20+
declaration: {
21+
name: referencePropertyName(primaryIdentifier[0], resource.name),
22+
type: Type.STRING,
23+
immutable: true,
24+
docs: {
25+
summary: `The ${primaryIdentifier[0]} of the ${resource.name} resource.`,
26+
},
27+
},
28+
cfnValue: $this.ref,
29+
});
30+
} else if (primaryIdentifier.length > 1) {
31+
for (const [i, cfnName] of enumerate(primaryIdentifier)) {
32+
referenceProps.push({
33+
declaration: {
34+
name: referencePropertyName(cfnName, resource.name),
35+
type: Type.STRING,
36+
immutable: true,
37+
docs: {
38+
summary: `The ${cfnName} of the ${resource.name} resource.`,
39+
},
40+
},
41+
cfnValue: splitSelect('|', i, $this.ref),
42+
});
43+
}
44+
}
45+
46+
const arnProp = findArnProperty(resource);
47+
if (arnProp) {
48+
referenceProps.push({
49+
declaration: {
50+
name: referencePropertyName(arnProp, resource.name),
51+
type: Type.STRING,
52+
immutable: true,
53+
docs: {
54+
summary: `The ARN of the ${resource.name} resource.`,
55+
},
56+
},
57+
cfnValue: $this[attributePropertyName(arnProp)],
58+
});
59+
}
60+
return referenceProps;
61+
}
62+
63+
/**
64+
* Find an ARN property for a given resource
65+
*
66+
* Returns `undefined` if no ARN property is found, or if the ARN property is already
67+
* included in the primary identifier.
68+
*/
69+
export function findArnProperty(resource: Resource) {
70+
const possibleArnNames = ['Arn', `${resource.name}Arn`];
71+
for (const name of possibleArnNames) {
72+
const prop = resource.attributes[name];
73+
if (prop && !resource.primaryIdentifier?.includes(name)) {
74+
return name;
75+
}
76+
}
77+
return undefined;
78+
}
79+
80+
function splitSelect(sep: string, n: number, base: Expression) {
81+
return CDK_CORE.Fn.select(expr.lit(n), CDK_CORE.Fn.split(expr.lit(sep), base));
82+
}
83+
84+
function enumerate<A>(xs: A[]): Array<[number, A]> {
85+
return xs.map((x, i) => [i, x]);
86+
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Property, RelationshipRef, Resource, RichProperty, SpecDatabase } from '@aws-cdk/service-spec-types';
2+
import { namespaceFromResource, referenceInterfaceName, referenceInterfaceAttributeName, referencePropertyName, typeAliasPrefixFromResource } from '../naming';
3+
import { getReferenceProps } from './reference-props';
4+
import { createModuleDefinitionFromCfnNamespace } from '../cfn2ts/pkglint';
5+
import { log } from '../util';
6+
7+
/**
8+
* Represents a cross-service property relationship that enables references
9+
* between resources from different AWS services.
10+
*/
11+
export interface Relationship {
12+
/** The TypeScript interface type that provides the reference (e.g. "IRoleRef") */
13+
readonly referenceType: string;
14+
/** The property name on the reference interface that holds the reference object (e.g. "roleRef") */
15+
readonly referenceName: string;
16+
/** The property to extract from the reference object (e.g. "roleArn") */
17+
readonly propName: string;
18+
}
19+
20+
/**
21+
* Represents a selective import statement for cross-module type references.
22+
* Used to import specific types from other CDK modules when relationships
23+
* are between different modules.
24+
*/
25+
export interface SelectiveImport {
26+
/** The module name to import from */
27+
readonly moduleName: string;
28+
/** Array of types that need to be imported */
29+
readonly types: {
30+
/** The original type name in the source module */
31+
originalType: string;
32+
/** The aliased name to avoid naming conflicts */
33+
aliasedType: string;
34+
}[];
35+
}
36+
37+
/**
38+
* Extracts resource relationship information from the database for cross-service property references.
39+
*/
40+
export class RelationshipDecider {
41+
private readonly namespace: string;
42+
public readonly imports = new Array<SelectiveImport>();
43+
44+
constructor(readonly resource: Resource, private readonly db: SpecDatabase) {
45+
this.namespace = namespaceFromResource(resource);
46+
}
47+
48+
private registerRequiredImport({ namespace, originalType, aliasedType }: {
49+
namespace: string;
50+
originalType: string;
51+
aliasedType: string;
52+
}) {
53+
const moduleName = createModuleDefinitionFromCfnNamespace(namespace).moduleName;
54+
const moduleImport = this.imports.find(i => i.moduleName === moduleName);
55+
if (!moduleImport) {
56+
this.imports.push({
57+
moduleName,
58+
types: [{ originalType, aliasedType }],
59+
});
60+
} else {
61+
if (!moduleImport.types.find(t =>
62+
t.originalType === originalType && t.aliasedType === aliasedType,
63+
)) {
64+
moduleImport.types.push({ originalType, aliasedType });
65+
}
66+
}
67+
}
68+
69+
/**
70+
* Retrieves the target resource for a relationship.
71+
* Returns undefined if the target property cannot be found in the reference
72+
* properties as a relationship can only target a primary identifier or arn
73+
*/
74+
private findTargetResource(sourcePropName: string, relationship: RelationshipRef) {
75+
const targetResource = this.db.lookup('resource', 'cloudFormationType', 'equals', relationship.cloudFormationType).only();
76+
const refProps = getReferenceProps(targetResource);
77+
const expectedPropName = referencePropertyName(relationship.propertyName, targetResource.name);
78+
const prop = refProps.find(p => p.declaration.name === expectedPropName);
79+
if (!prop) {
80+
log.debug(
81+
'Could not find target prop for relationship:',
82+
`${this.resource.cloudFormationType} ${sourcePropName}`,
83+
`=> ${targetResource.cloudFormationType} ${relationship.propertyName}`,
84+
);
85+
return undefined;
86+
}
87+
return targetResource;
88+
}
89+
90+
public parseRelationship(sourcePropName: string, relationships?: RelationshipRef[]) {
91+
const parsedRelationships: Relationship[] = [];
92+
if (!relationships) {
93+
return parsedRelationships;
94+
}
95+
for (const relationship of relationships) {
96+
const targetResource = this.findTargetResource(sourcePropName, relationship);
97+
if (!targetResource) {
98+
continue;
99+
}
100+
// Ignore the suffix part because it's an edge case that happens only for one module
101+
const interfaceName = referenceInterfaceName(targetResource.name);
102+
const refPropStructName = referenceInterfaceAttributeName(targetResource.name);
103+
104+
const targetNamespace = namespaceFromResource(targetResource);
105+
let aliasedTypeName = undefined;
106+
if (this.namespace !== targetNamespace) {
107+
// If this is not in our namespace we need to alias the import type
108+
aliasedTypeName = `${typeAliasPrefixFromResource(targetResource)}${interfaceName}`;
109+
this.registerRequiredImport({ namespace: targetNamespace, originalType: interfaceName, aliasedType: aliasedTypeName });
110+
}
111+
parsedRelationships.push({
112+
referenceType: aliasedTypeName ?? interfaceName,
113+
referenceName: refPropStructName,
114+
propName: referencePropertyName(relationship.propertyName, targetResource.name),
115+
});
116+
}
117+
return parsedRelationships;
118+
}
119+
120+
/**
121+
* Extracts the referenced type from a property's type, for direct refs and array element refs.
122+
*/
123+
private getReferencedType(prop: Property) {
124+
// Use the oldest type for backwards compatibility
125+
const type = new RichProperty(prop).types()[0];
126+
if (type.type === 'ref') {
127+
return this.db.get('typeDefinition', type.reference.$ref);
128+
} else if (type.type === 'array' && type.element.type === 'ref') {
129+
return this.db.get('typeDefinition', type.element.reference.$ref);
130+
}
131+
return undefined;
132+
}
133+
134+
private hasValidRelationships(sourcePropName: string, relationships?: RelationshipRef[]): boolean {
135+
if (!relationships) {
136+
return false;
137+
}
138+
return relationships.some(rel => this.findTargetResource(sourcePropName, rel) !== undefined);
139+
}
140+
141+
/**
142+
* Checks if a given property needs a flattening function or not
143+
*/
144+
public needsFlatteningFunction(propName: string, prop: Property, visited = new Set<string>()): boolean {
145+
if (this.hasValidRelationships(propName, prop.relationshipRefs)) {
146+
return true;
147+
}
148+
149+
const referencedTypeDef = this.getReferencedType(prop);
150+
if (!referencedTypeDef) {
151+
return false;
152+
}
153+
154+
if (visited.has(referencedTypeDef.$id)) {
155+
return false;
156+
}
157+
visited.add(referencedTypeDef.$id);
158+
159+
for (const [nestedPropName, nestedProp] of Object.entries(referencedTypeDef.properties)) {
160+
if (this.needsFlatteningFunction(nestedPropName, nestedProp, visited)) {
161+
return true;
162+
}
163+
}
164+
return false;
165+
}
166+
}

0 commit comments

Comments
 (0)