@cdktn/hcl2cdk 0.24.0-pre.45 → 0.24.0-pre.47

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/LICENSE +355 -0
  2. package/README.md +1 -1
  3. package/build/__tests__/expressions.test.js +10 -19
  4. package/build/__tests__/functions.test.js +8 -18
  5. package/build/__tests__/testHelpers.js +3 -2
  6. package/build/coerceType.js +11 -21
  7. package/build/dynamic-blocks.js +3 -3
  8. package/build/expressions.js +13 -22
  9. package/build/function-bindings/functions.generated.js +2 -2
  10. package/build/generation.js +24 -34
  11. package/build/index.js +15 -25
  12. package/build/iteration.js +7 -6
  13. package/build/jsii-rosetta-workarounds.js +6 -5
  14. package/build/partialCode.js +11 -20
  15. package/build/provider.js +4 -3
  16. package/build/references.js +6 -5
  17. package/build/schema.js +8 -18
  18. package/build/terraformSchema.js +4 -4
  19. package/build/utils.js +3 -3
  20. package/build/variables.js +12 -21
  21. package/package.json +20 -17
  22. package/package.sh +1 -1
  23. package/src/__tests__/coerceType.test.ts +207 -0
  24. package/src/__tests__/expressionToTs.test.ts +1167 -0
  25. package/src/__tests__/expressions.test.ts +541 -0
  26. package/src/__tests__/findExpressionType.test.ts +112 -0
  27. package/src/__tests__/functions.test.ts +768 -0
  28. package/src/__tests__/generation.test.ts +72 -0
  29. package/src/__tests__/jsii-rosetta-workarounds.test.ts +145 -0
  30. package/src/__tests__/partialCode.test.ts +432 -0
  31. package/src/__tests__/terraformSchema.test.ts +107 -0
  32. package/src/__tests__/testHelpers.ts +11 -0
  33. package/src/coerceType.ts +261 -0
  34. package/src/dynamic-blocks.ts +61 -0
  35. package/src/expressions.ts +968 -0
  36. package/src/function-bindings/functions.generated.ts +1139 -0
  37. package/src/function-bindings/functions.ts +104 -0
  38. package/src/generation.ts +1189 -0
  39. package/src/index.ts +584 -0
  40. package/src/iteration.ts +156 -0
  41. package/src/jsii-rosetta-workarounds.ts +145 -0
  42. package/src/partialCode.ts +132 -0
  43. package/src/provider.ts +60 -0
  44. package/src/references.ts +193 -0
  45. package/src/schema.ts +74 -0
  46. package/src/terraformSchema.ts +182 -0
  47. package/src/types.ts +58 -0
  48. package/src/utils.ts +19 -0
  49. package/src/variables.ts +214 -0
  50. package/test/__snapshots__/backends.test.ts.snap +70 -0
  51. package/test/__snapshots__/externals.test.ts.snap +37 -0
  52. package/test/__snapshots__/granular-imports.test.ts.snap +180 -0
  53. package/test/__snapshots__/imports.test.ts.snap +159 -0
  54. package/test/__snapshots__/iteration.test.ts.snap +532 -0
  55. package/test/__snapshots__/jsiiLanguage.test.ts.snap +347 -0
  56. package/test/__snapshots__/locals.test.ts.snap +55 -0
  57. package/test/__snapshots__/modules.test.ts.snap +127 -0
  58. package/test/__snapshots__/outputs.test.ts.snap +77 -0
  59. package/test/__snapshots__/partialCode.test.ts.snap +120 -0
  60. package/test/__snapshots__/provider.test.ts.snap +128 -0
  61. package/test/__snapshots__/references.test.ts.snap +376 -0
  62. package/test/__snapshots__/resource-meta-properties.test.ts.snap +342 -0
  63. package/test/__snapshots__/resources.test.ts.snap +613 -0
  64. package/test/__snapshots__/tfExpressions.test.ts.snap +537 -0
  65. package/test/__snapshots__/typeCoercion.test.ts.snap +253 -0
  66. package/test/__snapshots__/variables.test.ts.snap +150 -0
  67. package/test/backends.test.ts +75 -0
  68. package/test/convertProject.test.ts +257 -0
  69. package/test/externals.test.ts +35 -0
  70. package/test/globalSetup.ts +224 -0
  71. package/test/globalTeardown.ts +11 -0
  72. package/test/granular-imports.test.ts +161 -0
  73. package/test/hcl2cdk.test.ts +88 -0
  74. package/test/helpers/convert.ts +543 -0
  75. package/test/helpers/tmp.ts +25 -0
  76. package/test/imports.test.ts +141 -0
  77. package/test/iteration.test.ts +342 -0
  78. package/test/jsiiLanguage.test.ts +73 -0
  79. package/test/locals.test.ts +47 -0
  80. package/test/modules.test.ts +143 -0
  81. package/test/outputs.test.ts +69 -0
  82. package/test/partialCode.test.ts +25 -0
  83. package/test/provider.test.ts +106 -0
  84. package/test/references.test.ts +287 -0
  85. package/test/resource-meta-properties.test.ts +288 -0
  86. package/test/resources.test.ts +551 -0
  87. package/test/tfExpressions.test.ts +300 -0
  88. package/test/typeCoercion.test.ts +154 -0
  89. package/test/variables.test.ts +96 -0
@@ -0,0 +1,1167 @@
1
+ /**
2
+ * Copyright (c) HashiCorp, Inc.
3
+ * SPDX-License-Identifier: MPL-2.0
4
+ */
5
+
6
+ import { convertTerraformExpressionToTs } from "../expressions";
7
+ import { ResourceScope } from "../types";
8
+ import { AttributeType } from "@cdktn/commons";
9
+ import { astToCode as code } from "./testHelpers";
10
+
11
+ const awsProviderSchema = {
12
+ format_version: "1.0",
13
+ provider_schemas: {
14
+ "registry.terraform.io/hashicorp/aws": {
15
+ provider: {},
16
+ resource_schemas: {
17
+ aws_s3_bucket: {
18
+ block: {
19
+ attributes: {
20
+ foo: {
21
+ type: ["map", "string"],
22
+ description_kind: "plain",
23
+ },
24
+ },
25
+ },
26
+ },
27
+ },
28
+ data_source_schemas: {
29
+ aws_availability_zones: {
30
+ version: 0,
31
+ block: {
32
+ attributes: {
33
+ all_availability_zones: {
34
+ type: "bool",
35
+ description_kind: "plain",
36
+ optional: true,
37
+ },
38
+ exclude_names: {
39
+ type: ["set", "string"],
40
+ description_kind: "plain",
41
+ optional: true,
42
+ },
43
+ exclude_zone_ids: {
44
+ type: ["set", "string"],
45
+ description_kind: "plain",
46
+ optional: true,
47
+ },
48
+ group_names: {
49
+ type: ["set", "string"],
50
+ description_kind: "plain",
51
+ computed: true,
52
+ },
53
+ id: {
54
+ type: "string",
55
+ description_kind: "plain",
56
+ optional: true,
57
+ computed: true,
58
+ },
59
+ names: {
60
+ type: ["list", "string"],
61
+ description_kind: "plain",
62
+ computed: true,
63
+ },
64
+ state: {
65
+ type: "string",
66
+ description_kind: "plain",
67
+ optional: true,
68
+ },
69
+ zone_ids: {
70
+ type: ["list", "string"],
71
+ description_kind: "plain",
72
+ computed: true,
73
+ },
74
+ testing_map: {
75
+ type: ["map", "string"],
76
+ description_kind: "plain",
77
+ computed: true,
78
+ },
79
+ },
80
+ block_types: {
81
+ filter: {
82
+ nesting_mode: "set",
83
+ block: {
84
+ attributes: {
85
+ name: {
86
+ type: "string",
87
+ description_kind: "plain",
88
+ required: true,
89
+ },
90
+ values: {
91
+ type: ["set", "string"],
92
+ description_kind: "plain",
93
+ required: true,
94
+ },
95
+ },
96
+ description_kind: "plain",
97
+ },
98
+ },
99
+ timeouts: {
100
+ nesting_mode: "single",
101
+ block: {
102
+ attributes: {
103
+ read: {
104
+ type: "string",
105
+ description_kind: "plain",
106
+ optional: true,
107
+ },
108
+ },
109
+ description_kind: "plain",
110
+ },
111
+ },
112
+ },
113
+ description_kind: "plain",
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ };
120
+
121
+ const externalProviderSchema = {
122
+ format_version: "1.0",
123
+ provider_schemas: {
124
+ "registry.terraform.io/hashicorp/external": {
125
+ provider: {},
126
+ resource_schemas: {},
127
+ data_source_schemas: {
128
+ external: {
129
+ version: 0,
130
+ block: {
131
+ attributes: {
132
+ id: {
133
+ type: "string",
134
+ description:
135
+ "The id of the data source. This will always be set to `-`",
136
+ description_kind: "plain",
137
+ computed: true,
138
+ },
139
+ program: {
140
+ type: ["list", "string"],
141
+ description:
142
+ "A list of strings, whose first element is the program to run and whose subsequent elements are optional command line arguments to the program. Terraform does not execute the program through a shell, so it is not necessary to escape shell metacharacters nor add quotes around arguments containing spaces.",
143
+ description_kind: "plain",
144
+ required: true,
145
+ },
146
+ query: {
147
+ type: ["map", "string"],
148
+ description:
149
+ "A map of string values to pass to the external program as the query arguments. If not supplied, the program will receive an empty object as its input.",
150
+ description_kind: "plain",
151
+ optional: true,
152
+ },
153
+ result: {
154
+ type: ["map", "string"],
155
+ description:
156
+ "A map of string values returned from the external program.",
157
+ description_kind: "plain",
158
+ computed: true,
159
+ },
160
+ working_dir: {
161
+ type: "string",
162
+ description:
163
+ "Working directory of the program. If not supplied, the program will run in the current directory.",
164
+ description_kind: "plain",
165
+ optional: true,
166
+ },
167
+ },
168
+ description:
169
+ 'The `external` data source allows an external program implementing a specific protocol (defined below) to act as a data source, exposing arbitrary data for use elsewhere in the Terraform configuration.\n\n**Warning** This mechanism is provided as an "escape hatch" for exceptional situations where a first-class Terraform provider is not more appropriate. Its capabilities are limited in comparison to a true data source, and implementing a data source via an external program is likely to hurt the portability of your Terraform configuration by creating dependencies on external programs and libraries that may not be available (or may need to be used differently) on different operating systems.\n\n**Warning** Terraform Enterprise does not guarantee availability of any particular language runtimes or external programs beyond standard shell utilities, so it is not recommended to use this data source within configurations that are applied within Terraform Enterprise.',
170
+ description_kind: "plain",
171
+ },
172
+ },
173
+ },
174
+ },
175
+ },
176
+ };
177
+
178
+ type GetScopeParams = {
179
+ provider?: Record<string, any>;
180
+ resources?: string[];
181
+ data?: string[];
182
+ variables?: string[];
183
+ locals?: string[];
184
+ forEachIteratorName?: string;
185
+ withinOverrideExpression?: boolean;
186
+ scopedVariables?: Record<string, string>;
187
+ };
188
+
189
+ function getScope({
190
+ provider,
191
+ resources,
192
+ data,
193
+ variables,
194
+ locals,
195
+ forEachIteratorName,
196
+ scopedVariables,
197
+ withinOverrideExpression = false,
198
+ }: GetScopeParams = {}): ResourceScope {
199
+ const scopeVariables: ResourceScope["variables"] = {};
200
+
201
+ resources?.forEach((varName) => {
202
+ let [resourceType, resourceName] = varName.split(".");
203
+ if (!resourceName) {
204
+ resourceName = varName;
205
+ resourceType = "aws_s3_bucket";
206
+ }
207
+
208
+ scopeVariables[varName] = {
209
+ resource: resourceType,
210
+ variableName: resourceName,
211
+ };
212
+ });
213
+
214
+ data?.forEach((varName) => {
215
+ scopeVariables[varName] = {
216
+ resource: "data.aws_s3_bucket",
217
+ variableName: varName,
218
+ };
219
+ });
220
+
221
+ variables?.forEach((varName) => {
222
+ scopeVariables[varName] = {
223
+ resource: "var",
224
+ variableName: varName,
225
+ };
226
+ });
227
+
228
+ locals?.forEach((varName) => {
229
+ scopeVariables[varName] = {
230
+ resource: "local",
231
+ variableName: varName,
232
+ };
233
+ });
234
+
235
+ const scope = {
236
+ providerSchema: provider ?? {},
237
+ providerGenerator: {},
238
+ constructs: new Set<string>(),
239
+ variables: scopeVariables,
240
+ hasTokenBasedTypeCoercion: true,
241
+ scopedVariables: scopedVariables || {},
242
+ forEachIteratorName,
243
+ withinOverrideExpression,
244
+ nodeIds: [],
245
+ importables: [],
246
+ topLevelConfig: {},
247
+ };
248
+
249
+ return scope;
250
+ }
251
+
252
+ const getType = (): AttributeType => "string";
253
+
254
+ describe("expressionToTs", () => {
255
+ test("converts a simple string to a string", async () => {
256
+ const expression = "hello";
257
+ const scope = getScope();
258
+ const result = await convertTerraformExpressionToTs(
259
+ scope,
260
+ expression,
261
+ getType,
262
+ );
263
+ expect(code(result)).toMatchInlineSnapshot(`""hello""`);
264
+ });
265
+
266
+ test("converts a simple expression with a template", async () => {
267
+ const expression = "${22}";
268
+ const scope = getScope();
269
+ const result = await convertTerraformExpressionToTs(
270
+ scope,
271
+ expression,
272
+ getType,
273
+ );
274
+ expect(code(result)).toMatchInlineSnapshot(`"Token.asString(22)"`);
275
+ });
276
+
277
+ test("converts a variable reference", async () => {
278
+ const expression = "${var.foo}";
279
+ const scope = getScope();
280
+ const result = await convertTerraformExpressionToTs(
281
+ scope,
282
+ expression,
283
+ getType,
284
+ );
285
+ expect(code(result)).toMatchInlineSnapshot(`"foo.stringValue"`);
286
+ });
287
+
288
+ test("convert a variable reference with snake case", async () => {
289
+ const expression = "${var.foo_bar}";
290
+ const scope = getScope({ variables: ["foo_bar"] });
291
+ const result = await convertTerraformExpressionToTs(
292
+ scope,
293
+ expression,
294
+ getType,
295
+ );
296
+ expect(code(result)).toMatchInlineSnapshot(
297
+ `"Token.asString(fooBar.value)"`,
298
+ );
299
+ });
300
+
301
+ test("convert a function call", async () => {
302
+ const expression = `\${replace("hello", "l", "w")}`;
303
+ const scope = getScope();
304
+ const result = await convertTerraformExpressionToTs(
305
+ scope,
306
+ expression,
307
+ getType,
308
+ );
309
+ expect(code(result)).toMatchInlineSnapshot(
310
+ `"Token.asString(Fn.replace("hello", "l", "w"))"`,
311
+ );
312
+ });
313
+
314
+ test("converts a function call with an expression", async () => {
315
+ const expression = `\${replace("hello-\${22+22}", "44", "world")}`;
316
+ const scope = getScope();
317
+ const result = await convertTerraformExpressionToTs(
318
+ scope,
319
+ expression,
320
+ getType,
321
+ );
322
+ expect(code(result)).toMatchInlineSnapshot(
323
+ `"Token.asString(Fn.replace("hello-" + Token.asString(Op.add(22, 22)), "44", "world"))"`,
324
+ );
325
+ });
326
+
327
+ test("converts string concatenation of iterator key", async () => {
328
+ const expression = '${"dynamic-ingress-${ingress.key}"}';
329
+ const scope = getScope({
330
+ scopedVariables: {
331
+ ingress: "dynamic_iterator0",
332
+ },
333
+ });
334
+ const result = await convertTerraformExpressionToTs(
335
+ scope,
336
+ expression,
337
+ getType,
338
+ );
339
+ expect(code(result)).toMatchInlineSnapshot(
340
+ `""dynamic-ingress-\${" + Token.asString(dynamic_iterator0.key) + "}""`,
341
+ );
342
+ });
343
+
344
+ test("convert a variable reference", async () => {
345
+ const expression = `\${var.foo_bar}`;
346
+ const scope = getScope({ variables: ["foo_bar"] });
347
+ const result = await convertTerraformExpressionToTs(
348
+ scope,
349
+ expression,
350
+ getType,
351
+ );
352
+ // TODO: This seems broken
353
+ expect(code(result)).toMatchInlineSnapshot(
354
+ `"Token.asString(fooBar.value)"`,
355
+ );
356
+ });
357
+
358
+ test("convert a resource reference", async () => {
359
+ const expression = `"simple-\${aws_s3_bucket.foo.id}"`;
360
+ const scope = getScope({ resources: ["aws_s3_bucket.foo"] });
361
+ const result = await convertTerraformExpressionToTs(
362
+ scope,
363
+ expression,
364
+ getType,
365
+ );
366
+ expect(code(result)).toMatchInlineSnapshot(`""simple-\${" + foo.id + "}""`);
367
+ });
368
+
369
+ test("convert a resource reference with nested properties", async () => {
370
+ const expression = `"simple-\${aws_s3_bucket.foo.prop.test}"`;
371
+ const scope = getScope({ resources: ["aws_s3_bucket.foo"] });
372
+ const result = await convertTerraformExpressionToTs(
373
+ scope,
374
+ expression,
375
+ getType,
376
+ );
377
+ expect(code(result)).toMatchInlineSnapshot(
378
+ `""simple-\${" + foo.prop.test + "}""`,
379
+ );
380
+ });
381
+
382
+ test("convert a local reference", async () => {
383
+ const expression = `"simple-\${local.foo}"`;
384
+ const scope = getScope({ locals: ["foo"] });
385
+ const result = await convertTerraformExpressionToTs(
386
+ scope,
387
+ expression,
388
+ getType,
389
+ );
390
+ expect(code(result)).toMatchInlineSnapshot(`""simple-\${" + foo + "}""`);
391
+ });
392
+
393
+ test("plain resource references in arithmetics", async () => {
394
+ const expression =
395
+ "${aws_s3_bucket.examplebucket.count + aws_s3_bucket.otherbucket.count }";
396
+ const scope = getScope({
397
+ resources: ["aws_s3_bucket.examplebucket", "aws_s3_bucket.otherbucket"],
398
+ });
399
+ const result = await convertTerraformExpressionToTs(
400
+ scope,
401
+ expression,
402
+ getType,
403
+ );
404
+ expect(code(result)).toMatchInlineSnapshot(
405
+ `"Token.asString(Op.add(examplebucket.count, otherbucket.count))"`,
406
+ );
407
+ });
408
+
409
+ test("use fqn for splat reference", async () => {
410
+ const expression = `\${aws_s3_bucket.foo.*.id}`;
411
+ const scope = getScope({ resources: ["aws_s3_bucket.foo"] });
412
+ const result = await convertTerraformExpressionToTs(
413
+ scope,
414
+ expression,
415
+ getType,
416
+ );
417
+ expect(code(result)).toMatchInlineSnapshot(
418
+ `"Token.asString(Fn.lookupNested(foo, ["*", "id"]))"`,
419
+ );
420
+ });
421
+
422
+ test("use fqn if property is present on numeric access using dot notation", async () => {
423
+ const expression =
424
+ "${aws_s3_bucket.examplebucket.network_interface.0.access_config.0.assigned_nat_ip}";
425
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
426
+ const result = await convertTerraformExpressionToTs(
427
+ scope,
428
+ expression,
429
+ getType,
430
+ );
431
+ expect(code(result)).toMatchInlineSnapshot(
432
+ `"Token.asString(Fn.lookupNested(examplebucket.networkInterface, ["0", "access_config", "0", "assigned_nat_ip"]))"`,
433
+ );
434
+ });
435
+
436
+ test("use fqn if property is present on numeric access", async () => {
437
+ const expression =
438
+ "${aws_s3_bucket.examplebucket.network_interface[0].access_config[0].assigned_nat_ip}";
439
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
440
+ const result = await convertTerraformExpressionToTs(
441
+ scope,
442
+ expression,
443
+ getType,
444
+ );
445
+ expect(code(result)).toMatchInlineSnapshot(
446
+ `"Token.asString(Fn.lookupNested(examplebucket.networkInterface, ["0", "access_config", "0", "assigned_nat_ip"]))"`,
447
+ );
448
+ });
449
+
450
+ test("use no fqn if property is present on numeric access using []", async () => {
451
+ const expression =
452
+ "${aws_s3_bucket.examplebucket.network_interface[0].access_config[0].assigned_nat_ip}";
453
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
454
+ const result = await convertTerraformExpressionToTs(
455
+ scope,
456
+ expression,
457
+ getType,
458
+ );
459
+ expect(code(result)).toMatchInlineSnapshot(
460
+ `"Token.asString(Fn.lookupNested(examplebucket.networkInterface, ["0", "access_config", "0", "assigned_nat_ip"]))"`,
461
+ );
462
+ });
463
+
464
+ test("detects splat reference within function", async () => {
465
+ const expression = "${toset(aws_s3_bucket.examplebucket.*)}";
466
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
467
+ const result = await convertTerraformExpressionToTs(
468
+ scope,
469
+ expression,
470
+ getType,
471
+ );
472
+ expect(code(result)).toMatchInlineSnapshot(
473
+ `"Token.asString(Fn.toset(Fn.lookupNested(examplebucket, ["*"])))"`,
474
+ );
475
+ });
476
+
477
+ test("convert conditional", async () => {
478
+ const expression =
479
+ "${aws_kms_key.key.deletion_window_in_days > 3 ? aws_s3_bucket.examplebucket.id : []}";
480
+ const scope = getScope({ resources: ["aws_kms_key.key", "examplebucket"] });
481
+ const result = await convertTerraformExpressionToTs(
482
+ scope,
483
+ expression,
484
+ getType,
485
+ );
486
+ expect(code(result)).toMatchInlineSnapshot(
487
+ `"Token.asString(conditional(Op.gt(key.deletionWindowInDays, 3), examplebucket.id, []))"`,
488
+ );
489
+ });
490
+
491
+ test("converts a function with references", async () => {
492
+ const expression = "${element(aws_s3_bucket.examplebucket, 0).id}";
493
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
494
+ const result = await convertTerraformExpressionToTs(
495
+ scope,
496
+ expression,
497
+ getType,
498
+ );
499
+ expect(code(result)).toMatchInlineSnapshot(
500
+ `"Token.asString(Fn.lookupNested(Fn.element(examplebucket, 0), ["id"]))"`,
501
+ );
502
+ });
503
+
504
+ test("converts a function with splat reference", async () => {
505
+ const expression = "${element(aws_s3_bucket.examplebucket.*.id, 0)}";
506
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
507
+ const result = await convertTerraformExpressionToTs(
508
+ scope,
509
+ expression,
510
+ getType,
511
+ );
512
+ expect(code(result)).toMatchInlineSnapshot(
513
+ `"Token.asString(Fn.element(Fn.lookupNested(examplebucket, ["*", "id"]), 0))"`,
514
+ );
515
+ });
516
+
517
+ test("convert for loops", async () => {
518
+ const expression =
519
+ "${{ for name, user in var.users : user.role => name...}}";
520
+
521
+ const scope = getScope();
522
+ const result = await convertTerraformExpressionToTs(
523
+ scope,
524
+ expression,
525
+ getType,
526
+ );
527
+ expect(code(result)).toMatchInlineSnapshot(
528
+ `""\${{ for name, user in \${" + users.value + "} : user.role => name...}}""`,
529
+ );
530
+ });
531
+
532
+ test("for expressions", async () => {
533
+ const expression = "{for s in var.list : s => upper(s)}";
534
+
535
+ const scope = getScope();
536
+ const result = await convertTerraformExpressionToTs(
537
+ scope,
538
+ expression,
539
+ getType,
540
+ );
541
+ expect(code(result)).toMatchInlineSnapshot(
542
+ `""\${{ for s in \${" + list.value + "} : s => upper(s)}}""`,
543
+ );
544
+ });
545
+
546
+ test("for list expression ", async () => {
547
+ const expression = "[for s in var.list : upper(s)]";
548
+
549
+ const scope = getScope();
550
+ const result = await convertTerraformExpressionToTs(
551
+ scope,
552
+ expression,
553
+ getType,
554
+ );
555
+ expect(code(result)).toMatchInlineSnapshot(
556
+ `""\${[ for s in \${" + list.value + "} : upper(s)]}""`,
557
+ );
558
+ });
559
+
560
+ test("for list with conditional", async () => {
561
+ const expression = '[for s in var.list : upper(s) if s != ""]';
562
+ const scope = getScope();
563
+ const result = await convertTerraformExpressionToTs(
564
+ scope,
565
+ expression,
566
+ getType,
567
+ );
568
+ expect(code(result)).toMatchInlineSnapshot(
569
+ `""\${[ for s in \${" + list.value + "} : upper(s) if s != \\"\\"]}""`,
570
+ );
571
+ });
572
+
573
+ test("convert property access resources", async () => {
574
+ const expression = "${aws_s3_bucket.examplebucket[0].id}";
575
+ const scope = getScope({ resources: ["aws_s3_bucket.examplebucket"] });
576
+ const result = await convertTerraformExpressionToTs(
577
+ scope,
578
+ expression,
579
+ getType,
580
+ );
581
+ expect(code(result)).toMatchInlineSnapshot(
582
+ `"Token.asString(Fn.lookupNested(examplebucket, ["0", "id"]))"`,
583
+ );
584
+ });
585
+
586
+ test("convert unary operators", async () => {
587
+ const expression = `\${!var.enabled}`;
588
+ const scope = getScope({ variables: ["enabled"] });
589
+ const result = await convertTerraformExpressionToTs(
590
+ scope,
591
+ expression,
592
+ () => "bool",
593
+ );
594
+ expect(code(result)).toMatchInlineSnapshot(
595
+ `"Token.asBoolean(Op.not(enabled.value))"`,
596
+ );
597
+ });
598
+
599
+ test("convert function with arrays and comments", async () => {
600
+ const expression = `\${compact([
601
+ # The example "bucket"
602
+ aws_s3_bucket.examplebucket,
603
+
604
+ # The "Learn" single page application. This is not configured in all environments.
605
+ var.input,
606
+ ])}`;
607
+ const scope = getScope({
608
+ resources: ["aws_s3_bucket.examplebucket"],
609
+ variables: ["input"],
610
+ });
611
+ const result = await convertTerraformExpressionToTs(
612
+ scope,
613
+ expression,
614
+ getType,
615
+ );
616
+ // TODO: See if we have a way to preserve comments
617
+ expect(code(result)).toMatchInlineSnapshot(
618
+ `"Token.asString(Fn.compact(Token.asList([examplebucket, input.value])))"`,
619
+ );
620
+ });
621
+
622
+ test("convert references for same reference", async () => {
623
+ const expression = `\${var.input == "test" ? "azure-ad-int" : "azure-ad-\${var.input}"}`;
624
+ const scope = getScope({ variables: ["input"] });
625
+ const result = await convertTerraformExpressionToTs(
626
+ scope,
627
+ expression,
628
+ getType,
629
+ );
630
+ expect(code(result)).toMatchInlineSnapshot(
631
+ `"Token.asString(conditional(Op.eq(input.value, "test"), "azure-ad-int", "azure-ad-\${" + input.value + "}"))"`,
632
+ );
633
+ });
634
+
635
+ test("nested variable access", async () => {
636
+ const expression = `\${element(var.test2["val1"], 0)}`;
637
+ const scope = getScope({ variables: ["test2"] });
638
+ const result = await convertTerraformExpressionToTs(
639
+ scope,
640
+ expression,
641
+ getType,
642
+ );
643
+ expect(code(result)).toMatchInlineSnapshot(
644
+ `"Token.asString(Fn.element(Fn.lookupNested(test2.value, ["\\"val1\\""]), 0))"`,
645
+ );
646
+ });
647
+
648
+ test("complicated nested local value", async () => {
649
+ const expression = "${flatten(var.vnets[*].subnets[*].name)}";
650
+ const scope = getScope({ variables: ["vnets"] });
651
+ const result = await convertTerraformExpressionToTs(
652
+ scope,
653
+ expression,
654
+ getType,
655
+ );
656
+ expect(code(result)).toMatchInlineSnapshot(
657
+ `"Token.asString(Fn.flatten(Fn.lookupNested(vnets.value, ["*", "subnets", "*", "name"])))"`,
658
+ );
659
+ });
660
+
661
+ // TOIMPROVE: I don't think we can handle this case yet, since the variable
662
+ // needs to be wrapped in an iterator. I'm leaving this in, but it's broken
663
+ test("complicated nested variable access with map", async () => {
664
+ const expression = `{
665
+ for vnet in var.vnets[*]:
666
+ (vnet.vnet_name) => vnet.subnets[*].name
667
+ }`;
668
+ const scope = getScope({ variables: ["vnets"] });
669
+ const result = await convertTerraformExpressionToTs(
670
+ scope,
671
+ expression,
672
+ getType,
673
+ );
674
+ expect(code(result)).toMatchInlineSnapshot(
675
+ `""\${{ for vnet in \${" + Fn.lookupNested(vnets.value, ["*"]) + "} : (vnet.vnet_name) => vnet.subnets[*].name}}""`,
676
+ );
677
+ });
678
+
679
+ test("complicated nested variable access with list", async () => {
680
+ const expression = `\${flatten([
681
+ for k, v in var.route : [
682
+ for n, s in v : [
683
+ {
684
+ key = k,
685
+ name = n,
686
+ svc_url = s
687
+ }
688
+ ]
689
+ ]
690
+ ])}`;
691
+ const scope = getScope({ variables: ["route"] });
692
+ const result = await convertTerraformExpressionToTs(
693
+ scope,
694
+ expression,
695
+ getType,
696
+ );
697
+ expect(code(result)).toMatchInlineSnapshot(
698
+ `"Token.asString(Fn.flatten("\${[ for k, v in \${" + route.value + "} : [\\n for n, s in v : [\\n {\\n key = k,\\n name = n,\\n svc_url = s\\n }\\n ]\\n ]]}"))"`,
699
+ );
700
+ });
701
+
702
+ test("simple local variable access", async () => {
703
+ const expression = "${!local.enabled}";
704
+ const scope = getScope({ locals: ["enabled"] });
705
+ const result = await convertTerraformExpressionToTs(
706
+ scope,
707
+ expression,
708
+ getType,
709
+ );
710
+ expect(code(result)).toMatchInlineSnapshot(
711
+ `"Token.asString(Op.not(enabled))"`,
712
+ );
713
+ });
714
+
715
+ test("multi local access", async () => {
716
+ const expression =
717
+ '"${local.service_name},${local.owner},${local.is_it_great},${local.how_many}"';
718
+ const scope = getScope({
719
+ locals: ["service_name", "owner", "is_it_great", "how_many"],
720
+ });
721
+ const result = await convertTerraformExpressionToTs(
722
+ scope,
723
+ expression,
724
+ getType,
725
+ );
726
+ expect(code(result)).toMatchInlineSnapshot(
727
+ `""\${" + serviceName + "},\${" + owner + "},\${" + isItGreat + "},\${" + howMany + "}""`,
728
+ );
729
+ });
730
+
731
+ test("binary operations within literals and variables", async () => {
732
+ const expression = '"${var.test} + 1"';
733
+ const scope = getScope({ variables: ["test"] });
734
+ const result = await convertTerraformExpressionToTs(
735
+ scope,
736
+ expression,
737
+ getType,
738
+ );
739
+ expect(code(result)).toMatchInlineSnapshot(
740
+ `""\${" + test.value + "} + 1""`,
741
+ );
742
+ });
743
+
744
+ test("local variable access", async () => {
745
+ const expression = '"${local.enabled}"';
746
+ const scope = getScope({ locals: ["enabled"] });
747
+ const result = await convertTerraformExpressionToTs(
748
+ scope,
749
+ expression,
750
+ getType,
751
+ );
752
+ expect(code(result)).toMatchInlineSnapshot(`"enabled"`);
753
+ });
754
+
755
+ test("converts traversals that cannot be references", async () => {
756
+ const expression = "${self.path}";
757
+ const scope = getScope();
758
+ const result = await convertTerraformExpressionToTs(
759
+ scope,
760
+ expression,
761
+ getType,
762
+ );
763
+ expect(code(result)).toMatchInlineSnapshot(
764
+ `"Token.asString(TerraformSelf.getAny("path"))"`,
765
+ );
766
+ });
767
+
768
+ test("converts join function with variables", async () => {
769
+ const expression = '${join("-", [var.tags.app, var.tags.env])}';
770
+ const scope = getScope({ variables: ["tags"] });
771
+ const result = await convertTerraformExpressionToTs(
772
+ scope,
773
+ expression,
774
+ getType,
775
+ );
776
+ expect(code(result)).toMatchInlineSnapshot(
777
+ `"Token.asString(Fn.join("-", Token.asList([Fn.lookupNested(tags.value, ["app"]), Fn.lookupNested(tags.value, ["env"])])))"`,
778
+ );
779
+ });
780
+
781
+ test("converts join function with many arguments variables", async () => {
782
+ const expression =
783
+ '${join("-", var.tags.app, var.tags.env, var.tags.other)}';
784
+ const scope = getScope({ variables: ["tags"] });
785
+ const result = await convertTerraformExpressionToTs(
786
+ scope,
787
+ expression,
788
+ getType,
789
+ );
790
+ expect(code(result)).toMatchInlineSnapshot(
791
+ `"Token.asString(Fn.join("-", Token.asList(Fn.concat([Fn.lookupNested(tags.value, ["app"]), Fn.lookupNested(tags.value, ["env"]), Fn.lookupNested(tags.value, ["other"])]))))"`,
792
+ );
793
+ });
794
+
795
+ test("doesn't wrap any extra templates", async () => {
796
+ const expression = `"app-\${terraform.workspace}"`;
797
+ const scope = getScope();
798
+ const result = await convertTerraformExpressionToTs(
799
+ scope,
800
+ expression,
801
+ getType,
802
+ );
803
+ expect(code(result)).toMatchInlineSnapshot(
804
+ `""app-\${terraform.workspace}""`,
805
+ );
806
+ });
807
+
808
+ test("converts a for expression", async () => {
809
+ const expression = `[for record in aws_route53_record.example : record.fqdn]`;
810
+ const scope = getScope();
811
+ const result = await convertTerraformExpressionToTs(
812
+ scope,
813
+ expression,
814
+ getType,
815
+ );
816
+ expect(code(result)).toMatchInlineSnapshot(
817
+ `""\${[ for record in \${" + example.fqn + "} : record.fqdn]}""`,
818
+ );
819
+ });
820
+
821
+ test("converts a data source", async () => {
822
+ const expression = `"\${data.aws_route53_zone.example.zone_id}"`;
823
+ const scope = getScope();
824
+ const result = await convertTerraformExpressionToTs(
825
+ scope,
826
+ expression,
827
+ getType,
828
+ );
829
+ expect(code(result)).toMatchInlineSnapshot(
830
+ `"Token.asString(example.zoneId)"`,
831
+ );
832
+ });
833
+
834
+ test("convert an each expression", async () => {
835
+ const expression = `"\${each.value.name}"`;
836
+ const scope = getScope({ forEachIteratorName: "myIterator" });
837
+ const result = await convertTerraformExpressionToTs(
838
+ scope,
839
+ expression,
840
+ getType,
841
+ );
842
+ expect(code(result)).toMatchInlineSnapshot(
843
+ `"Token.asString(Fn.lookupNested(myIterator.value, ["name"]))"`,
844
+ );
845
+ });
846
+
847
+ test("converts a property of a function containing a resource", async () => {
848
+ const expression = `"\${element(aws_s3_bucket.examplebucket, 0).id}"`;
849
+ const scope = getScope();
850
+ const result = await convertTerraformExpressionToTs(
851
+ scope,
852
+ expression,
853
+ getType,
854
+ );
855
+ expect(code(result)).toMatchInlineSnapshot(
856
+ `"Token.asString(Fn.lookupNested(Fn.element(examplebucket, 0), ["id"]))"`,
857
+ );
858
+ });
859
+
860
+ test("convert a function with references with splats", async () => {
861
+ const expression = `"\${concat(var.private_subnets.*.id, var.public_subnets.*.id)}"`;
862
+ const scope = getScope({
863
+ variables: ["private_subnets", "public_subnets"],
864
+ });
865
+ const result = await convertTerraformExpressionToTs(
866
+ scope,
867
+ expression,
868
+ getType,
869
+ );
870
+ expect(code(result)).toMatchInlineSnapshot(
871
+ `"Token.asString(Fn.concat([Fn.lookupNested(privateSubnets.value, ["*", "id"]), Fn.lookupNested(publicSubnets.value, ["*", "id"])]))"`,
872
+ );
873
+ });
874
+
875
+ test("convert a iterator variable", async () => {
876
+ const expression = `"\${each.key}"`;
877
+ const scope = getScope({
878
+ variables: ["private_subnets", "public_subnets"],
879
+ forEachIteratorName: "myIterator",
880
+ });
881
+ const result = await convertTerraformExpressionToTs(
882
+ scope,
883
+ expression,
884
+ getType,
885
+ );
886
+ expect(code(result)).toMatchInlineSnapshot(
887
+ `"Token.asString(myIterator.key)"`,
888
+ );
889
+ });
890
+
891
+ test("convert a self reference", async () => {
892
+ const expression = `"\${self.subnet.id}"`;
893
+ const scope = getScope({
894
+ variables: ["private_subnets", "public_subnets"],
895
+ forEachIteratorName: "myIterator",
896
+ });
897
+ const result = await convertTerraformExpressionToTs(
898
+ scope,
899
+ expression,
900
+ getType,
901
+ );
902
+ expect(code(result)).toMatchInlineSnapshot(
903
+ `"Token.asString(TerraformSelf.getAny("subnet.id"))"`,
904
+ );
905
+ });
906
+
907
+ test("convert heredocs", async () => {
908
+ const expression = `<<EOF
909
+ [{
910
+ "Condition": {
911
+ "KeyPrefixEquals": "docs/"
912
+ },
913
+ "Redirect": {
914
+ "ReplaceKeyPrefixWith": "documents/"
915
+ }
916
+ }]
917
+ EOF
918
+ `;
919
+ const scope = getScope();
920
+ const result = await convertTerraformExpressionToTs(
921
+ scope,
922
+ expression,
923
+ getType,
924
+ );
925
+ expect(code(result)).toMatchInlineSnapshot(
926
+ `""[{\\n \\"Condition\\": {\\n \\"KeyPrefixEquals\\": \\"docs/\\"\\n },\\n \\"Redirect\\": {\\n \\"ReplaceKeyPrefixWith\\": \\"documents/\\"\\n }\\n}]\\n""`,
927
+ );
928
+ });
929
+
930
+ test("convert heredocs without a trailing newline", async () => {
931
+ const expression = `<<EOF
932
+ hello world
933
+ EOF`;
934
+ const scope = getScope();
935
+ const result = await convertTerraformExpressionToTs(
936
+ scope,
937
+ expression,
938
+ getType,
939
+ );
940
+ expect(code(result)).toMatchInlineSnapshot(`""hello world\\n""`);
941
+ });
942
+
943
+ test("convert indented heredocs", async () => {
944
+ const expression = `<<-EOF
945
+ hello world
946
+ EOF`;
947
+ const scope = getScope();
948
+ const result = await convertTerraformExpressionToTs(
949
+ scope,
950
+ expression,
951
+ getType,
952
+ );
953
+ expect(code(result)).toMatchInlineSnapshot(`""hello world\\n""`);
954
+ });
955
+
956
+ test("convert override expressions", async () => {
957
+ const expression = '"${required_resource_access.value["resource_access"]}"';
958
+ const scope = getScope({
959
+ withinOverrideExpression: true,
960
+ scopedVariables: {
961
+ required_resource_access: "dynamic-block",
962
+ },
963
+ });
964
+ const result = await convertTerraformExpressionToTs(
965
+ scope,
966
+ expression,
967
+ getType,
968
+ );
969
+ expect(code(result)).toMatchInlineSnapshot(
970
+ `""\${required_resource_access.value[\\"resource_access\\"]}""`,
971
+ );
972
+ });
973
+
974
+ test("convert using operations while containing substring", async () => {
975
+ const expression =
976
+ // prettier-ignore
977
+ '"${length(var.image_id) > 4 && substr(var.image_id, 0, 4) == "ami-"}"';
978
+ const scope = getScope({});
979
+ const result = await convertTerraformExpressionToTs(
980
+ scope,
981
+ expression,
982
+ getType,
983
+ );
984
+ expect(code(result)).toMatchInlineSnapshot(
985
+ `"Token.asString(Op.and(Op.gt(Fn.lengthOf(imageId.value), 4), Op.eq(Fn.substr(imageId.stringValue, 0, 4), "ami-")))"`,
986
+ );
987
+ });
988
+
989
+ test("convert a data source with numeric access", async () => {
990
+ const expression = `"\${data.aws_availability_zones.changeme_az_list_ebs_snapshot.names[0]}"`;
991
+ const scope = getScope({
992
+ provider: awsProviderSchema,
993
+ data: ["aws_subnet_ids"],
994
+ });
995
+ const result = await convertTerraformExpressionToTs(
996
+ scope,
997
+ expression,
998
+ getType,
999
+ );
1000
+ expect(code(result)).toMatchInlineSnapshot(
1001
+ `"Token.asString(Fn.lookupNested(changemeAzListEbsSnapshot.names, ["0"]))"`,
1002
+ );
1003
+ });
1004
+
1005
+ test("convert a reference to a map access", async () => {
1006
+ const expression = `"\${data.aws_availability_zones.changeme_az_list_ebs_snapshot.testing_map.foo}"`;
1007
+ const scope = getScope({
1008
+ provider: awsProviderSchema,
1009
+ data: ["aws_subnet_ids"],
1010
+ });
1011
+ const result = await convertTerraformExpressionToTs(
1012
+ scope,
1013
+ expression,
1014
+ getType,
1015
+ );
1016
+ expect(code(result)).toMatchInlineSnapshot(
1017
+ `"Token.asString(Fn.lookupNested(changemeAzListEbsSnapshot, ["testing_map", "foo"]))"`,
1018
+ );
1019
+ });
1020
+
1021
+ test("convert attribute up to a map and not within attribute reference", async () => {
1022
+ const expression = `"\${data.aws_availability_zones.changeme_az_list_ebs_snapshot.testing_map}"`;
1023
+ const scope = getScope({
1024
+ provider: awsProviderSchema,
1025
+ });
1026
+
1027
+ const result = await convertTerraformExpressionToTs(
1028
+ scope,
1029
+ expression,
1030
+ () => ["map", "string"],
1031
+ );
1032
+
1033
+ expect(code(result)).toMatchInlineSnapshot(
1034
+ `"Token.asStringMap(changemeAzListEbsSnapshot.testingMap)"`,
1035
+ );
1036
+ });
1037
+
1038
+ test("don't convert external unknown fields", async () => {
1039
+ const expression = `"\${data.external.changeme_external_thumbprint_data.result.thumbprint}"`;
1040
+ const scope = getScope({
1041
+ provider: externalProviderSchema,
1042
+ data: ["aws_subnet_ids"],
1043
+ });
1044
+
1045
+ const result = await convertTerraformExpressionToTs(
1046
+ scope,
1047
+ expression,
1048
+ () => ["map", "string"],
1049
+ );
1050
+ expect(code(result)).toMatchInlineSnapshot(
1051
+ `"Token.asStringMap(Fn.lookupNested(changemeExternalThumbprintData, ["result", "thumbprint"]))"`,
1052
+ );
1053
+ });
1054
+
1055
+ test("convert resource reference with map attribute", async () => {
1056
+ const expression = `"\${aws_s3_bucket.examplebucket.foo.bar}"`;
1057
+ const scope = getScope({
1058
+ provider: awsProviderSchema,
1059
+ resources: ["aws_s3_bucket"],
1060
+ });
1061
+ const result = await convertTerraformExpressionToTs(
1062
+ scope,
1063
+ expression,
1064
+ () => "string",
1065
+ );
1066
+ expect(code(result)).toMatchInlineSnapshot(
1067
+ `"Token.asString(Fn.lookupNested(examplebucket, ["foo", "bar"]))"`,
1068
+ );
1069
+ });
1070
+
1071
+ test("convert complex variable reference in template", async () => {
1072
+ const expression = `"\${var.default_tags.project}-client-tg"`;
1073
+ const scope = getScope({
1074
+ provider: awsProviderSchema,
1075
+ resources: ["aws_s3_bucket"],
1076
+ variables: ["default_tags"],
1077
+ });
1078
+ const result = await convertTerraformExpressionToTs(
1079
+ scope,
1080
+ expression,
1081
+ () => "string",
1082
+ );
1083
+ expect(code(result)).toMatchInlineSnapshot(
1084
+ `"Token.asString(Fn.lookupNested(defaultTags.value, ["project"])) + "-client-tg""`,
1085
+ );
1086
+ });
1087
+
1088
+ test("convert boolean values", async () => {
1089
+ const expression = `"\${false}"`;
1090
+ const scope = getScope({
1091
+ provider: awsProviderSchema,
1092
+ });
1093
+ const result = await convertTerraformExpressionToTs(
1094
+ scope,
1095
+ expression,
1096
+ () => "bool",
1097
+ );
1098
+ expect(code(result)).toMatchInlineSnapshot(`"false"`);
1099
+ });
1100
+
1101
+ test("convert a data source with count", async () => {
1102
+ const expression = `"\${data.aws_availability_zones.available.names[count.index]}"`;
1103
+ const scope = getScope({
1104
+ provider: awsProviderSchema,
1105
+ data: ["aws_availability_zones"],
1106
+ });
1107
+ const result = await convertTerraformExpressionToTs(
1108
+ scope,
1109
+ expression,
1110
+ getType,
1111
+ );
1112
+ expect(code(result)).toMatchInlineSnapshot(
1113
+ `"Token.asString(Fn.lookupNested(available.names, ["\${count.index}"]))"`,
1114
+ );
1115
+ });
1116
+
1117
+ test("accept escaped quotes within string", async () => {
1118
+ const expression = `"\${jsonencode({
1119
+ "Statement" = [{
1120
+ "Action" = "s3:*",
1121
+ "Effect" = "Allow",
1122
+ }],
1123
+ })}"`;
1124
+ const scope = getScope();
1125
+ const result = await convertTerraformExpressionToTs(
1126
+ scope,
1127
+ expression,
1128
+ getType,
1129
+ );
1130
+ expect(code(result)).toMatchInlineSnapshot(`
1131
+ "Token.asString(Fn.jsonencode({
1132
+ "Statement": [{
1133
+ "Action": "s3:*",
1134
+ "Effect": "Allow"
1135
+ }]
1136
+ }))"
1137
+ `);
1138
+ });
1139
+
1140
+ test("support variadic parameters in any position", async () => {
1141
+ const expression = `"\${cidrsubnets("fd00:fd12:3456:7890::/56", 16, 16, 16, 32)}"`;
1142
+ const scope = getScope();
1143
+ const result = await convertTerraformExpressionToTs(
1144
+ scope,
1145
+ expression,
1146
+ getType,
1147
+ );
1148
+
1149
+ expect(code(result)).toMatchInlineSnapshot(
1150
+ `"Token.asString(Fn.cidrsubnets("fd00:fd12:3456:7890::/56", [16, 16, 16, 32]))"`,
1151
+ );
1152
+ });
1153
+
1154
+ test("converts template expression with escaped ${} expression", async () => {
1155
+ const expression = '["$${path:name.givenName}"]'; // from aws_ssoadmin_instance_access_control_attributes example
1156
+ const scope = getScope();
1157
+ const result = await convertTerraformExpressionToTs(
1158
+ scope,
1159
+ expression,
1160
+ getType,
1161
+ );
1162
+
1163
+ expect(code(result)).toMatchInlineSnapshot(
1164
+ `"["$\${path:name.givenName}"]"`,
1165
+ );
1166
+ });
1167
+ });