From a8ece2a385d959df2ccb00968df2e81cfc55fe9a Mon Sep 17 00:00:00 2001 From: Leon Michalski Date: Fri, 31 Oct 2025 17:38:28 +0100 Subject: [PATCH] core(spec2cdk): ensure strings are passed to properties --- packages/aws-cdk-lib/core/lib/runtime.ts | 11 ++++++ .../spec2cdk/lib/cdk/relationship-decider.ts | 3 ++ .../spec2cdk/lib/cdk/resolver-builder.ts | 7 +++- .../__snapshots__/relationships.test.ts.snap | 34 +++++++++---------- 4 files changed, 37 insertions(+), 18 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/runtime.ts b/packages/aws-cdk-lib/core/lib/runtime.ts index 00c459f81b528..2e0d8f1b33ff2 100644 --- a/packages/aws-cdk-lib/core/lib/runtime.ts +++ b/packages/aws-cdk-lib/core/lib/runtime.ts @@ -400,3 +400,14 @@ function isCloudFormationDynamicReference(x: any) { class CfnSynthesisError extends Error { public readonly type = 'CfnSynthesisError'; } + +/** + * Ensures that a property is either undefined or a string. + * Used in spec2cdk to have better error messages in other languages. + */ +export function ensureStringOrUndefined(value: any, propName: string, possibleType: string): string | undefined { + if (value !== undefined && typeof value !== 'string') { + throw new TypeError(`Property ${propName} should be one of ${possibleType}`); + } + return value; +} diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts index b4df6af5b207d..d50a177541a8f 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/relationship-decider.ts @@ -18,6 +18,8 @@ export interface Relationship { readonly referenceName: string; /** The property to extract from the reference object (e.g. "roleArn") */ readonly propName: string; + /** Human friendly name of the reference type for error generation (e.g. "iam.IRoleRef") */ + readonly typeDisplayName: string; } /** @@ -118,6 +120,7 @@ export class RelationshipDecider { referenceType: aliasedTypeName ?? interfaceName, referenceName: refPropStructName, propName: referencePropertyName(relationship.propertyName, targetResource.name), + typeDisplayName: `${typeAliasPrefixFromResource(targetResource).toLowerCase()}.${interfaceName}`, }); } return parsedRelationships; diff --git a/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts b/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts index 1dca3489dc9e1..8a1287ddecdde 100644 --- a/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts +++ b/tools/@aws-cdk/spec2cdk/lib/cdk/resolver-builder.ts @@ -74,6 +74,11 @@ export class ResolverBuilder { ? Type.arrayOf(Type.distinctUnionOf(resolvableType.arrayOfType, ...newTypes)) : Type.distinctUnionOf(resolvableType, ...newTypes); + const typeDisplayNames = [ + ...relationships.map(r => r.typeDisplayName), + resolvableType.arrayOfType?.toString() ?? resolvableType.toString(), + ].join(' | '); + // Generates code like: // For single value: (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? (props.roleArn as IUserRef)?.userRef?.userArn ?? props.roleArn // For array: props.roleArns?.map((item: any) => (item as IRoleRef)?.roleRef?.roleArn ?? (item as IUserRef)?.userRef?.userArn ?? item) @@ -84,7 +89,7 @@ export class ResolverBuilder { const buildChain = (itemName: string) => [ ...[...arnRels, ...otherRels] .map(r => `(${itemName} as ${r.referenceType})?.${r.referenceName}?.${r.propName}`), - itemName, + `cdk.ensureStringOrUndefined(${itemName}, "${name}", "${typeDisplayNames}")`, ].join(' ?? '); const resolver = (_: Expression) => { if (resolvableType.arrayOfType) { diff --git a/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap b/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap index a1f1b2c0dd21b..466475a2a6545 100644 --- a/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap +++ b/tools/@aws-cdk/spec2cdk/test/__snapshots__/relationships.test.ts.snap @@ -12,7 +12,7 @@ import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; * * @stability experimental */ -export interface IRoleRef extends constructs.IConstruct { +export interface IRoleRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Role resource. */ @@ -165,7 +165,7 @@ function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface IResourceRef extends constructs.IConstruct { +export interface IResourceRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Resource resource. */ @@ -280,7 +280,7 @@ export interface CfnResourceProps { function flattenCfnResourcePermissionProperty(props: cdk.IResolvable | CfnResource.PermissionProperty): cdk.IResolvable | CfnResource.PermissionProperty { if (cdk.isResolvableObject(props)) return props; return { - "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? cdk.ensureStringOrUndefined(props.roleArn, "roleArn", "iam.IRoleRef | string") }; } @@ -391,7 +391,7 @@ import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; * * @stability experimental */ -export interface IRoleRef extends constructs.IConstruct { +export interface IRoleRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Role resource. */ @@ -544,7 +544,7 @@ function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface IUserRef extends constructs.IConstruct { +export interface IUserRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a User resource. */ @@ -697,7 +697,7 @@ function CfnUserPropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface IPolicyRef extends constructs.IConstruct { +export interface IPolicyRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Policy resource. */ @@ -752,7 +752,7 @@ export class CfnPolicy extends cdk.CfnResource implements cdk.IInspectable, IPol "properties": props }); - this.principalArn = (props.principalArn as IRoleRef)?.roleRef?.roleArn ?? (props.principalArn as IUserRef)?.userRef?.userArn ?? props.principalArn; + this.principalArn = (props.principalArn as IRoleRef)?.roleRef?.roleArn ?? (props.principalArn as IUserRef)?.userRef?.userArn ?? cdk.ensureStringOrUndefined(props.principalArn, "principalArn", "iam.IRoleRef | iam.IUserRef | string"); } public get policyRef(): PolicyReference { @@ -859,7 +859,7 @@ import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; * * @stability experimental */ -export interface IRoleRef extends constructs.IConstruct { +export interface IRoleRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Role resource. */ @@ -1012,7 +1012,7 @@ function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface ITaskRef extends constructs.IConstruct { +export interface ITaskRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Task resource. */ @@ -1127,7 +1127,7 @@ export interface CfnTaskProps { function flattenCfnTaskExecutionConfigProperty(props: CfnTask.ExecutionConfigProperty | cdk.IResolvable): CfnTask.ExecutionConfigProperty | cdk.IResolvable { if (cdk.isResolvableObject(props)) return props; return { - "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? cdk.ensureStringOrUndefined(props.roleArn, "roleArn", "iam.IRoleRef | string") }; } @@ -1238,7 +1238,7 @@ import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; * * @stability experimental */ -export interface IRoleRef extends constructs.IConstruct { +export interface IRoleRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Role resource. */ @@ -1391,7 +1391,7 @@ function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface IJobRef extends constructs.IConstruct { +export interface IJobRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Job resource. */ @@ -1523,7 +1523,7 @@ export interface CfnJobProps { function flattenCfnJobConfigProperty(props: CfnJob.ConfigProperty | cdk.IResolvable): CfnJob.ConfigProperty | cdk.IResolvable { if (cdk.isResolvableObject(props)) return props; return { - "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn, + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? cdk.ensureStringOrUndefined(props.roleArn, "roleArn", "iam.IRoleRef | string"), "timeout": props.timeout }; } @@ -1577,7 +1577,7 @@ function CfnJobConfigPropertyFromCloudFormation(properties: any): cfn_parse.From function flattenCfnJobOldConfigProperty(props: cdk.IResolvable | CfnJob.OldConfigProperty): cdk.IResolvable | CfnJob.OldConfigProperty { if (cdk.isResolvableObject(props)) return props; return { - "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn + "roleArn": (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? cdk.ensureStringOrUndefined(props.roleArn, "roleArn", "iam.IRoleRef | string") }; } @@ -1688,7 +1688,7 @@ import * as cdk_errors from "aws-cdk-lib/core/lib/errors"; * * @stability experimental */ -export interface IRoleRef extends constructs.IConstruct { +export interface IRoleRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Role resource. */ @@ -1841,7 +1841,7 @@ function CfnRolePropsFromCloudFormation(properties: any): cfn_parse.FromCloudFor * * @stability experimental */ -export interface IFunctionRef extends constructs.IConstruct { +export interface IFunctionRef extends constructs.IConstruct, cdk.IEnvironmentAware { /** * A reference to a Function resource. */ @@ -1896,7 +1896,7 @@ export class CfnFunction extends cdk.CfnResource implements cdk.IInspectable, IF "properties": props }); - this.roleArn = (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? props.roleArn; + this.roleArn = (props.roleArn as IRoleRef)?.roleRef?.roleArn ?? cdk.ensureStringOrUndefined(props.roleArn, "roleArn", "iam.IRoleRef | string"); } public get functionRef(): FunctionReference {