Skip to content

Commit

Permalink
feat(lib): add accessors for for expressions
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielMSchmidt committed Nov 23, 2023
1 parent 96d5772 commit ed42caa
Show file tree
Hide file tree
Showing 11 changed files with 2,709 additions and 298 deletions.
90 changes: 90 additions & 0 deletions packages/cdktf/lib/terraform-iterator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { ITerraformResource } from "./terraform-resource";
import {
FOR_EXPRESSION_KEY,
FOR_EXPRESSION_VALUE,
forExpression,
propertyAccess,
ref,
} from "./tfExpression";
Expand Down Expand Up @@ -235,6 +236,13 @@ export abstract class TerraformIterator implements ITerraformIterator {
);
}

/**
* Creates a dynamic expression that can be used to loop over this iterator
* in a dynamic block.
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
* See https://developer.hashicorp.com/terraform/cdktf/concepts/iterators#using-iterators-for-list-attributes
*/
public dynamic(attributes: { [key: string]: any }): IResolvable {
return Token.asAny(
new TerraformDynamicExpression({
Expand All @@ -243,6 +251,88 @@ export abstract class TerraformIterator implements ITerraformIterator {
})
);
}

/**
* Creates a for expression that maps the iterators to its keys.
* For lists these would be the indices, for maps the keys.
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
*/
public mapToKey(): IResolvable {
return Token.asAny(
forExpression(this._getForEachExpression(), FOR_EXPRESSION_KEY)
);
}

/**
* Creates a for expression that maps the iterators to its value in case it is a map.
* For lists these would stay the same.
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
*/
public mapToValue(): IResolvable {
return Token.asAny(
forExpression(this._getForEachExpression(), FOR_EXPRESSION_VALUE)
);
}

/**
* Creates a for expression that takes the given key from the values of this iterator.
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
* @param property The property of the iterators values to map to
*/
public mapToValueProperty(property: string): IResolvable {
return Token.asAny(
forExpression(
this._getForEachExpression(),
propertyAccess(FOR_EXPRESSION_VALUE, [property])
)
);
}

/**
* Creates a for expression that results in a list.
* This method allows you to create every possible for expression, but requires more knowledge about
* Terraforms for expression syntax.
* For the most common use cases you can use mapToKey(), mapToValue(), and mapToValueProperty instead.
*
* You may write any valid Terraform for each expression, it will result
* in `[ for key, val in var.myIteratorSource: <expression> ]`.
*
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
* @param expression The expression to use in the for mapping
*/
public forExpressionForList(expression: string | IResolvable) {
return Token.asAny(forExpression(this._getForEachExpression(), expression));
}

/**
* Creates a for expression that results in a map.
* This method allows you to create every possible for expression, but requires more knowledge about
* Terraforms for expression syntax.
* For the most common use cases you can use mapToKey(), mapToValue(), and mapToValueProperty instead.
*
* You may write any valid Terraform for each expression, it will result
* in `{ for key, val in var.myIteratorSource: <keyExpression> => <valueExpression> }`.
*
* As this returns an IResolvable you might need to wrap the output in
* a Token, e.g. `Token.asString`.
* @param expression The expression to use in the for mapping
*/
public forExpressionForMap(
keyExpression: string | IResolvable,
valueExpression: string | IResolvable
) {
return Token.asAny(
forExpression(
this._getForEachExpression(),
valueExpression,
keyExpression
)
);
}
}

// eslint-disable-next-line jsdoc/require-jsdoc
Expand Down
58 changes: 46 additions & 12 deletions packages/cdktf/lib/tfExpression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,11 @@ const TERRAFORM_IDENTIFIER_REGEX = /^[_a-zA-Z][_a-zA-Z0-9]*$/;

// eslint-disable-next-line jsdoc/require-jsdoc
class TFExpression extends Intrinsic implements IResolvable {
protected resolveArg(context: IResolveContext, arg: any): string {
protected resolveExpressionPart(context: IResolveContext, arg: any): string {
const resolvedArg = context.resolve(arg);
if (Tokenization.isResolvable(arg)) {
return resolvedArg;
}

if (typeof arg === "string") {
return this.resolveString(arg, resolvedArg);
}

if (Array.isArray(resolvedArg)) {
return `[${resolvedArg
.map((_, index) => this.resolveArg(context, arg[index]))
Expand All @@ -37,6 +32,16 @@ class TFExpression extends Intrinsic implements IResolvable {
return resolvedArg;
}

protected resolveArg(context: IResolveContext, arg: any): string {
const resolvedArg = context.resolve(arg);

if (typeof arg === "string") {
return this.resolveString(arg, resolvedArg);
}

return this.resolveExpressionPart(context, arg);
}

/**
* Escape string removes characters from the string that are not allowed in Terraform or JSON
* It must only be used on non-token values
Expand Down Expand Up @@ -108,6 +113,21 @@ class TFExpression extends Intrinsic implements IResolvable {
: `"${joinResult}"`;
}
}
// A string that represents an input value NOT to be escaped
// eslint-disable-next-line jsdoc/require-jsdoc
class UnescapedString extends TFExpression {
constructor(private readonly str: string) {
super(str);
}

public resolve() {
return this.str;
}

public toString() {
return this.str;
}
}

// A string that represents an input value to be escaped
// eslint-disable-next-line jsdoc/require-jsdoc
Expand All @@ -131,6 +151,11 @@ export function rawString(str: string): IResolvable {
return new RawString(str);
}

// eslint-disable-next-line jsdoc/require-jsdoc
export function unescapedString(str: string): IResolvable {
return new UnescapedString(str);
}

// eslint-disable-next-line jsdoc/require-jsdoc
class Reference extends TFExpression {
/**
Expand Down Expand Up @@ -194,12 +219,16 @@ export function insideTfExpression(arg: any) {

// eslint-disable-next-line jsdoc/require-jsdoc
class PropertyAccess extends TFExpression {
constructor(private target: Expression, private args: Expression[]) {
constructor(
private target: Expression,
private args: Expression[],
private forceNoBraces = false
) {
super({ target, args });
}

public resolve(context: IResolveContext): string {
const suppressBraces = context.suppressBraces;
const suppressBraces = this.forceNoBraces || context.suppressBraces;
context.suppressBraces = true;

const serializedArgs = this.args
Expand Down Expand Up @@ -234,8 +263,12 @@ class PropertyAccess extends TFExpression {
}

// eslint-disable-next-line jsdoc/require-jsdoc
export function propertyAccess(target: Expression, args: Expression[]) {
return new PropertyAccess(target, args) as IResolvable;
export function propertyAccess(
target: Expression,
args: Expression[],
forceNoBraces = false
) {
return new PropertyAccess(target, args, forceNoBraces) as IResolvable;
}

// eslint-disable-next-line jsdoc/require-jsdoc
Expand Down Expand Up @@ -388,11 +421,12 @@ class ForExpression extends TFExpression {
const key = this.resolveArg(context, FOR_EXPRESSION_KEY);
const value = this.resolveArg(context, FOR_EXPRESSION_VALUE);
const input = this.resolveArg(context, this.input);
const valueExpr = this.resolveArg(context, this.valueExpression);
const valueExpr = this.resolveExpressionPart(context, this.valueExpression);

let expr: string;
if (this.keyExpression) {
const keyExpr = this.resolveArg(context, this.keyExpression);
const keyExpr = this.resolveExpressionPart(context, this.keyExpression);

expr = `{ for ${key}, ${value} in ${input}: ${keyExpr} => ${valueExpr} }`;
} else {
expr = `[ for ${key}, ${value} in ${input}: ${valueExpr}]`;
Expand Down
49 changes: 49 additions & 0 deletions packages/cdktf/test/iterator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
Fn,
TerraformHclModule,
TerraformCount,
TerraformVariable,
Token,
} from "../lib";
import { TestResource } from "./helper";
import { TestDataSource } from "./helper/data-source";
Expand Down Expand Up @@ -452,3 +454,50 @@ test("chained iterators used with count", () => {
`"Cannot create iterator from resource with count argument. Please use the same TerraformCount used in the resource passed here instead."`
);
});

test("for expressions from iterators", () => {
const app = Testing.app();
const stack = new TerraformStack(app, "test");
const variable = new TerraformVariable(stack, "list", {});
const it = TerraformIterator.fromList(variable.listValue);
new TestResource(stack, "test", {
name: "foo",
tags: {
// Take a value from items of the list
arnProperties: Token.asString(it.mapToValueProperty("arn")),
// Filter out empty values
owners: Token.asString(
it.forExpressionForList(`val.owner if val.owner != ""`)
),

// Filter out teams with no members and join them with a comma
teams: Token.asString(
it.forExpressionForMap(
"val.teamName",
`join(",", val.teamMembers) if length(val.teamMembers) > 0`
)
),
// Get the keys of the map
keys: Token.asString(it.mapToKey()),
},
});

const synth = JSON.parse(Testing.synth(stack));
expect(synth).toHaveProperty(
"resource.test_resource.test.tags.arnProperties",
"${[ for key, val in toset(var.list): val.arn]}"
);
expect(synth).toHaveProperty(
"resource.test_resource.test.tags.owners",
'${[ for key, val in toset(var.list): val.owner if val.owner != ""]}'
);

expect(synth).toHaveProperty(
"resource.test_resource.test.tags.teams",
`\${{ for key, val in toset(var.list): val.teamName => join(",", val.teamMembers) if length(val.teamMembers) > 0 }}`
);
expect(synth).toHaveProperty(
"resource.test_resource.test.tags.keys",
"${[ for key, val in toset(var.list): key]}"
);
});
Loading

0 comments on commit ed42caa

Please sign in to comment.