@cdktn/provider-generator 0.24.0-pre.6 → 0.24.0-pre.61

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 (67) hide show
  1. package/.spec.swcrc +22 -0
  2. package/LICENSE +355 -0
  3. package/README.md +1 -1
  4. package/build/__tests__/edge-provider-schema/cli.js +8 -3
  5. package/build/get/__tests__/generator/import-style.test.d.ts +2 -0
  6. package/build/get/__tests__/generator/import-style.test.js +101 -0
  7. package/build/get/__tests__/generator/module-generator.test.js +12 -12
  8. package/build/get/constructs-maker.d.ts +5 -1
  9. package/build/get/constructs-maker.js +14 -6
  10. package/build/get/generator/emitter/struct-emitter.d.ts +2 -1
  11. package/build/get/generator/emitter/struct-emitter.js +14 -11
  12. package/build/get/generator/module-generator.js +3 -3
  13. package/build/get/generator/provider-generator.d.ts +9 -1
  14. package/build/get/generator/provider-generator.js +9 -6
  15. package/jest.config.js +16 -9
  16. package/package.json +24 -23
  17. package/package.sh +1 -1
  18. package/src/__tests__/__snapshots__/edge-provider-schema.test.ts.snap +8 -8
  19. package/src/__tests__/__snapshots__/provider.test.ts.snap +2951 -2951
  20. package/src/__tests__/edge-provider-schema/builder.ts +185 -0
  21. package/src/__tests__/edge-provider-schema/cli.ts +51 -0
  22. package/src/__tests__/edge-provider-schema/index.ts +161 -0
  23. package/src/__tests__/edge-provider-schema.test.ts +24 -0
  24. package/src/__tests__/provider.test.ts +180 -0
  25. package/src/get/__tests__/constructs-maker.test.ts +118 -0
  26. package/src/get/__tests__/generator/__snapshots__/complex-computed-types.test.ts.snap +5 -5
  27. package/src/get/__tests__/generator/__snapshots__/export-sharding.test.ts.snap +3310 -3310
  28. package/src/get/__tests__/generator/__snapshots__/module-generator.test.ts.snap +355 -355
  29. package/src/get/__tests__/generator/__snapshots__/nested-types.test.ts.snap +8 -8
  30. package/src/get/__tests__/generator/__snapshots__/provider.test.ts.snap +8 -8
  31. package/src/get/__tests__/generator/__snapshots__/resource-types.test.ts.snap +126 -126
  32. package/src/get/__tests__/generator/__snapshots__/skipped-attributes.test.ts.snap +17 -17
  33. package/src/get/__tests__/generator/__snapshots__/types.test.ts.snap +65 -65
  34. package/src/get/__tests__/generator/complex-computed-types.test.ts +28 -0
  35. package/src/get/__tests__/generator/deep-nested-attributes.test.ts +56 -0
  36. package/src/get/__tests__/generator/description-escaping.test.ts +84 -0
  37. package/src/get/__tests__/generator/empty-provider-resources.test.ts +26 -0
  38. package/src/get/__tests__/generator/export-sharding.test.ts +169 -0
  39. package/src/get/__tests__/generator/import-style.test.ts +129 -0
  40. package/src/get/__tests__/generator/module-generator.test.ts +528 -0
  41. package/src/get/__tests__/generator/nested-types.test.ts +54 -0
  42. package/src/get/__tests__/generator/provider.test.ts +51 -0
  43. package/src/get/__tests__/generator/resource-types.test.ts +118 -0
  44. package/src/get/__tests__/generator/skipped-attributes.test.ts +72 -0
  45. package/src/get/__tests__/generator/types.test.ts +611 -0
  46. package/src/get/__tests__/generator/versions-file.test.ts +72 -0
  47. package/src/get/__tests__/util.ts +75 -0
  48. package/src/get/constructs-maker.ts +822 -0
  49. package/src/get/generator/custom-defaults.ts +493 -0
  50. package/src/get/generator/emitter/attributes-emitter.ts +225 -0
  51. package/src/get/generator/emitter/index.ts +5 -0
  52. package/src/get/generator/emitter/resource-emitter.ts +226 -0
  53. package/src/get/generator/emitter/struct-emitter.ts +683 -0
  54. package/src/get/generator/loop-detection.ts +81 -0
  55. package/src/get/generator/models/attribute-model.ts +216 -0
  56. package/src/get/generator/models/attribute-type-model.ts +448 -0
  57. package/src/get/generator/models/index.ts +7 -0
  58. package/src/get/generator/models/resource-model.ts +161 -0
  59. package/src/get/generator/models/scope.ts +54 -0
  60. package/src/get/generator/models/struct.ts +116 -0
  61. package/src/get/generator/module-generator.ts +234 -0
  62. package/src/get/generator/provider-generator.ts +355 -0
  63. package/src/get/generator/resource-parser.ts +762 -0
  64. package/src/get/generator/sanitized-comments.ts +49 -0
  65. package/src/get/generator/skipped-attributes.ts +27 -0
  66. package/src/index.ts +40 -0
  67. package/src/util.ts +26 -0
@@ -0,0 +1,762 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import { toCamelCase, toPascalCase, toSnakeCase } from "codemaker";
4
+ import {
5
+ Attribute,
6
+ AttributeNestedType,
7
+ AttributeType,
8
+ Block,
9
+ BlockType,
10
+ ConstructsMakerTarget,
11
+ isAttributeNestedType,
12
+ isNestedTypeAttribute,
13
+ Schema,
14
+ } from "@cdktn/commons";
15
+ import { ProviderName, FQPN, parseFQPN } from "@cdktn/provider-schema";
16
+ import {
17
+ ResourceModel,
18
+ AttributeTypeModel,
19
+ Struct,
20
+ Scope,
21
+ AttributeModel,
22
+ SimpleAttributeTypeModel,
23
+ ListAttributeTypeModel,
24
+ SetAttributeTypeModel,
25
+ MapAttributeTypeModel,
26
+ StructAttributeTypeModel,
27
+ SkippedAttributeTypeModel,
28
+ } from "./models";
29
+ import { detectAttributeLoops } from "./loop-detection";
30
+ import { shouldSkipAttribute } from "./skipped-attributes";
31
+
32
+ // Can't be used in expressions like "export * as <keyword> from ... "
33
+ // filtered from all keywords from: https://github.com/microsoft/TypeScript/blob/503604c884bd0557c851b11b699ef98cdb65b93b/src/compiler/types.ts#L114-L197
34
+ const RESERVED_KEYWORDS_FOR_NAMESPACES = [
35
+ "implements",
36
+ "interface",
37
+ "let",
38
+ "package",
39
+ "private",
40
+ "protected",
41
+ "public",
42
+ "static",
43
+ "yield",
44
+ "await",
45
+ ];
46
+
47
+ const COLLIDING_NAMESPACE_NAMES = [
48
+ // e.g. hashicorp/consul – collides with the LICENSE file on case-insensitive filesystems in the Go package (#2627)
49
+ "license",
50
+ // collides for Go packages
51
+ "version",
52
+ ];
53
+
54
+ const isReservedClassOrNamespaceName = (className: string): boolean => {
55
+ return [
56
+ "string",
57
+ "object",
58
+ "function",
59
+ ...RESERVED_KEYWORDS_FOR_NAMESPACES,
60
+ ...COLLIDING_NAMESPACE_NAMES,
61
+ ].includes(className.toLowerCase());
62
+ };
63
+
64
+ const isReservedStructClassName = (className: string): boolean => {
65
+ return className.toLowerCase().endsWith("list");
66
+ };
67
+
68
+ const getFileName = (provider: ProviderName, baseName: string): string => {
69
+ if (baseName === "index") {
70
+ return "index-resource/index.ts";
71
+ }
72
+
73
+ if (baseName === `${provider}_provider`) {
74
+ return "provider/index.ts";
75
+ }
76
+
77
+ return `${toSnakeCase(baseName).replace(/_/g, "-")}/index.ts`;
78
+ };
79
+
80
+ export function sanitizeClassOrNamespaceName(
81
+ baseName: string,
82
+ isProvider = false,
83
+ ) {
84
+ const resourceIsNamedProvider = !isProvider && baseName === "provider";
85
+
86
+ if (isReservedClassOrNamespaceName(baseName) || resourceIsNamedProvider) {
87
+ return `${baseName}_resource`;
88
+ } else {
89
+ return baseName;
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Remove attributes that may conflict after being snake cased
95
+ * Example: oci_core_ipsec_connection_tunnel_management (hashicorp/oci@=5.21.0) has bgp_ipv6_state and bgp_ipv6state
96
+ * (which both result in "bgpIpv6State" when camel-cased, with the second one being deprecated: true)
97
+ * As we currently don't handle any deprecated ones at all, we'll just delete one of the two attributes for now
98
+ * @param attributes
99
+ */
100
+ function deduplicateAttributesWithSameName(
101
+ attributes: AttributeModel[],
102
+ ): AttributeModel[] {
103
+ return attributes.filter((attr, idx) => {
104
+ const hasOtherWithSameName = attributes
105
+ .slice(idx + 1) // only search after the index of the current attribute to avoid deleting both
106
+ .some((other) => other.name === attr.name && other !== attr);
107
+ return !hasOtherWithSameName;
108
+ });
109
+ }
110
+
111
+ class Parser {
112
+ private structs = new Array<Struct>();
113
+
114
+ constructor(
115
+ private classNames: string[],
116
+ private usedBaseNames: Map<string, string>,
117
+ ) {}
118
+
119
+ private uniqueClassName(className: string): string {
120
+ if (this.classNames.includes(className)) {
121
+ className = `${className}A`;
122
+ }
123
+ this.classNames.push(className);
124
+ return className;
125
+ }
126
+
127
+ /**
128
+ * Ensure base names are unique case-insensitively.
129
+ *
130
+ * Affected languages:
131
+ * - Go: JSII-generated Go package names are fully lowercased, so
132
+ * `load-balancer-backendset` and `load-balancer-backend-set` both
133
+ * become package `loadbalancerbackendset`, causing a hard build failure:
134
+ * "case-insensitive file name collision".
135
+ * - C#: JSII generates PascalCase directories like
136
+ * `LoadBalancerBackendset/` and `LoadBalancerBackendSet/` which the C#
137
+ * compiler treats case-insensitively, causing CS2002 and CS0234 errors.
138
+ */
139
+ private uniqueBaseName(baseName: string): string {
140
+ const pascalName = toPascalCase(baseName);
141
+ const key = pascalName.toLowerCase();
142
+ const existing = this.usedBaseNames.get(key);
143
+
144
+ if (existing !== undefined && existing !== pascalName) {
145
+ baseName = `${baseName}A`;
146
+ return this.uniqueBaseName(baseName);
147
+ }
148
+
149
+ this.usedBaseNames.set(key, pascalName);
150
+ return baseName;
151
+ }
152
+
153
+ public resourceFrom(
154
+ fqpn: FQPN,
155
+ type: string,
156
+ schema: Schema,
157
+ terraformSchemaType: string,
158
+ constraint?: ConstructsMakerTarget,
159
+ ): ResourceModel {
160
+ let baseName = type;
161
+
162
+ const providerNameFromConstraint = constraint
163
+ ? (constraint.name as ProviderName)
164
+ : undefined;
165
+ const providerNameFromFQPN = parseFQPN(fqpn).name;
166
+
167
+ if (baseName.startsWith(`${providerNameFromFQPN}_`)) {
168
+ baseName = baseName.substr(providerNameFromFQPN.length + 1);
169
+ }
170
+
171
+ const providerName = providerNameFromConstraint
172
+ ? providerNameFromConstraint
173
+ : providerNameFromFQPN;
174
+
175
+ const isProvider = terraformSchemaType === "provider";
176
+ if (isProvider) {
177
+ baseName = `${providerName}_${baseName}`;
178
+ if (!("attributes" in schema.block)) {
179
+ schema.block = {
180
+ attributes: {},
181
+ block_types: (schema.block as Block).block_types || {},
182
+ };
183
+ }
184
+ // somehow missing from provider schema
185
+ schema.block.attributes["alias"] = {
186
+ type: "string",
187
+ description: "Alias name",
188
+ optional: true,
189
+ computed: false,
190
+ };
191
+ }
192
+
193
+ baseName = sanitizeClassOrNamespaceName(baseName, isProvider);
194
+ baseName = this.uniqueBaseName(baseName);
195
+
196
+ const className = this.uniqueClassName(toPascalCase(baseName));
197
+ // avoid naming collision - see https://github.com/hashicorp/terraform-cdk/issues/299
198
+ const configStructName = this.uniqueClassName(`${className}Config`);
199
+ const fileName = getFileName(providerName, baseName);
200
+
201
+ const filePath = `providers/${toSnakeCase(providerName)}/${fileName}`;
202
+ let attributes = this.renderAttributesForBlock(
203
+ new Scope({
204
+ name: baseName,
205
+ isProvider,
206
+ parent: isProvider
207
+ ? undefined
208
+ : new Scope({ name: providerName, isProvider: true }),
209
+ }),
210
+ schema.block,
211
+ );
212
+
213
+ function getStructAttribute(
214
+ attributes: AttributeModel[],
215
+ path: string,
216
+ ): AttributeModel {
217
+ const [first, ...rest] = path.split(".");
218
+ const attribute = attributes.find((att) => {
219
+ return att.terraformName === first;
220
+ });
221
+ if (!attribute)
222
+ throw new Error(
223
+ `Expected to find recursive attribute at path: ${path}`,
224
+ );
225
+ if (!attribute.type.struct)
226
+ throw new Error(
227
+ `Expected to find struct type attribute at path: ${path} but got ${attribute.type.storedClassType}`,
228
+ );
229
+ if (rest.length === 0) return attribute;
230
+ return getStructAttribute(
231
+ attribute.type.struct.attributes,
232
+ rest.join("."),
233
+ );
234
+ }
235
+
236
+ // Introduce recursion for some attributes
237
+ const recursiveAttributePaths = detectAttributeLoops(attributes);
238
+
239
+ Object.entries(recursiveAttributePaths).forEach(
240
+ ([attributePath, structPath]) => {
241
+ // TODO: build this to be a bit more defensive (e.g. remove ! operator)
242
+ const recursionTargetStructAttribute = getStructAttribute(
243
+ attributes,
244
+ structPath,
245
+ );
246
+ const parts = attributePath.split(".");
247
+ const attributeName = parts.pop();
248
+ const parentAttribute = getStructAttribute(attributes, parts.join("."));
249
+ const indexToReplace =
250
+ parentAttribute.type.struct!.attributes.findIndex(
251
+ (att) => att.terraformName === attributeName,
252
+ );
253
+ if (indexToReplace === -1)
254
+ throw new Error("Can't find attribute at path " + attributePath);
255
+ const previousAttribute =
256
+ parentAttribute.type.struct!.attributes[indexToReplace];
257
+
258
+ parentAttribute.type.struct!.attributes[indexToReplace] =
259
+ recursionTargetStructAttribute; // introduce recursion
260
+
261
+ // ugly, pls c̶a̶l̶l̶ refactor me maybe
262
+ // we store all structs in this.structs – now we need to dispose all structs that are part of previousAttribute
263
+ const disposeStructs = (attr: AttributeModel) => {
264
+ if (attr.type.struct) {
265
+ attr.type.struct.attributes.forEach(disposeStructs);
266
+ this.structs = this.structs.filter((s) => s !== attr.type.struct);
267
+ }
268
+ };
269
+
270
+ disposeStructs(previousAttribute);
271
+ },
272
+ );
273
+
274
+ attributes = deduplicateAttributesWithSameName(attributes);
275
+
276
+ const resourceModel = new ResourceModel({
277
+ terraformType: type,
278
+ baseName,
279
+ fileName,
280
+ filePath,
281
+ className,
282
+ schema,
283
+ fqpn,
284
+ attributes,
285
+ terraformSchemaType,
286
+ structs: this.structs,
287
+ configStructName,
288
+ });
289
+
290
+ return resourceModel;
291
+ }
292
+
293
+ private renderAttributeType(
294
+ scope: Scope[],
295
+ attributeType: AttributeType | AttributeNestedType,
296
+ parentKind?: string,
297
+ ): AttributeTypeModel {
298
+ const parent = scope[scope.length - 1];
299
+ if (shouldSkipAttribute(parent.baseName)) {
300
+ return new MapAttributeTypeModel(new SimpleAttributeTypeModel("any"));
301
+ }
302
+
303
+ if (typeof attributeType === "string") {
304
+ switch (attributeType) {
305
+ case "bool":
306
+ return new SimpleAttributeTypeModel("boolean");
307
+ case "string":
308
+ return new SimpleAttributeTypeModel("string");
309
+ case "number":
310
+ return new SimpleAttributeTypeModel("number");
311
+ case "dynamic":
312
+ return new MapAttributeTypeModel(new SimpleAttributeTypeModel("any"));
313
+ default:
314
+ throw new Error(`invalid primitive type ${attributeType}`);
315
+ }
316
+ }
317
+
318
+ if (Array.isArray(attributeType)) {
319
+ if (attributeType.length !== 2) {
320
+ throw new Error(`unexpected array`);
321
+ }
322
+
323
+ const [kind, type] = attributeType;
324
+
325
+ if (kind === "set" || kind === "list") {
326
+ const elementType = this.renderAttributeType(
327
+ scope,
328
+ type as AttributeType,
329
+ [kind, parentKind].join(""),
330
+ );
331
+ return kind === "list"
332
+ ? new ListAttributeTypeModel(elementType, false, false)
333
+ : new SetAttributeTypeModel(elementType, false, false);
334
+ }
335
+
336
+ if (kind === "map") {
337
+ const valueType = this.renderAttributeType(
338
+ scope,
339
+ type as AttributeType,
340
+ [kind, parentKind].join(""),
341
+ );
342
+ return new MapAttributeTypeModel(valueType);
343
+ }
344
+
345
+ if (kind === "object") {
346
+ const objAttributes = type as { [name: string]: AttributeType };
347
+ const attributes: { [name: string]: Attribute } = {};
348
+ for (const [name, type] of Object.entries(objAttributes)) {
349
+ attributes[name] = { type };
350
+ }
351
+ const struct = this.addAnonymousStruct(
352
+ scope,
353
+ attributes,
354
+ parentKind ?? kind,
355
+ );
356
+ return new StructAttributeTypeModel(struct);
357
+ }
358
+ }
359
+
360
+ if (isAttributeNestedType(attributeType)) {
361
+ let struct = undefined;
362
+ let typeModel = undefined;
363
+ switch (attributeType.nesting_mode) {
364
+ case "list":
365
+ struct = this.addAnonymousStruct(
366
+ scope,
367
+ attributeType.attributes,
368
+ attributeType.nesting_mode,
369
+ );
370
+ typeModel = new ListAttributeTypeModel(
371
+ new StructAttributeTypeModel(struct),
372
+ false,
373
+ false,
374
+ );
375
+ break;
376
+ case "set":
377
+ struct = this.addAnonymousStruct(
378
+ scope,
379
+ attributeType.attributes,
380
+ attributeType.nesting_mode,
381
+ );
382
+ typeModel = new SetAttributeTypeModel(
383
+ new StructAttributeTypeModel(struct),
384
+ false,
385
+ false,
386
+ );
387
+ break;
388
+ case "map":
389
+ struct = this.addAnonymousStruct(
390
+ scope,
391
+ attributeType.attributes,
392
+ attributeType.nesting_mode,
393
+ );
394
+ typeModel = new MapAttributeTypeModel(
395
+ new StructAttributeTypeModel(struct),
396
+ );
397
+ break;
398
+ case "single":
399
+ struct = this.addAnonymousStruct(
400
+ scope,
401
+ attributeType.attributes,
402
+ attributeType.nesting_mode,
403
+ );
404
+ typeModel = new StructAttributeTypeModel(struct);
405
+ break;
406
+ default: {
407
+ throw new Error(
408
+ `nested_type with nesting_mode "${
409
+ attributeType.nesting_mode
410
+ }" not supported (attribute scope: ${scope
411
+ .map((s) => s.fullName)
412
+ .join(",")}`,
413
+ );
414
+ }
415
+ }
416
+ return typeModel;
417
+ }
418
+
419
+ throw new Error(`unknown type ${JSON.stringify(attributeType)}`);
420
+ }
421
+
422
+ public renderAttributesForBlock(parentType: Scope, block: Block) {
423
+ const attributes = new Array<AttributeModel>();
424
+
425
+ for (const [terraformAttributeName, att] of Object.entries(
426
+ block.attributes || {},
427
+ )) {
428
+ let type: AttributeTypeModel;
429
+ let forcePlainGetterType = false;
430
+ if (shouldSkipAttribute(parentType.fullName(terraformAttributeName))) {
431
+ type = new SkippedAttributeTypeModel();
432
+ forcePlainGetterType = true;
433
+ } else {
434
+ type = this.renderAttributeType(
435
+ [
436
+ parentType,
437
+ new Scope({
438
+ name: terraformAttributeName,
439
+ parent: parentType,
440
+ isProvider: parentType.isProvider,
441
+ isComputed: !!att.computed,
442
+ isOptional: !!att.optional,
443
+ isRequired: !!att.required,
444
+ isNestedType: isNestedTypeAttribute(att),
445
+ }),
446
+ ],
447
+ att.type || att.nested_type,
448
+ );
449
+ }
450
+
451
+ const name = toCamelCase(terraformAttributeName);
452
+
453
+ attributes.push(
454
+ new AttributeModel({
455
+ terraformFullName: parentType.fullName(terraformAttributeName),
456
+ description: att.description,
457
+ name,
458
+ storageName: `_${name}`,
459
+ computed: !!att.computed,
460
+ optional: !!att.optional,
461
+ terraformName: terraformAttributeName,
462
+ type,
463
+ provider: parentType.isProvider,
464
+ required: !!att.required,
465
+ forcePlainGetterType,
466
+ }),
467
+ );
468
+ }
469
+
470
+ for (const [blockTypeName, blockType] of Object.entries(
471
+ block.block_types || {},
472
+ )) {
473
+ if (shouldSkipAttribute(parentType.fullName(blockTypeName))) {
474
+ const name = toCamelCase(blockTypeName);
475
+ const parent = new Scope({
476
+ name: blockTypeName,
477
+ parent: parentType,
478
+ isProvider: parentType.isProvider,
479
+ });
480
+ attributes.push(
481
+ new AttributeModel({
482
+ name,
483
+ terraformName: blockTypeName,
484
+ terraformFullName: parent.fullName(blockTypeName),
485
+ type: new SkippedAttributeTypeModel(),
486
+ description: `${blockTypeName} block`,
487
+ storageName: `_${name}`,
488
+ optional: true,
489
+ computed: false,
490
+ provider: parentType.isProvider,
491
+ required: false,
492
+ }),
493
+ );
494
+ continue;
495
+ }
496
+ // create a struct for this block
497
+ let blockAttributes = this.renderAttributesForBlock(
498
+ new Scope({
499
+ name: `${parentType.name}_${blockTypeName}`,
500
+ parent: parentType,
501
+ isProvider: parentType.isProvider,
502
+ inBlockType: true,
503
+ }),
504
+ blockType.block,
505
+ );
506
+
507
+ blockAttributes = deduplicateAttributesWithSameName(blockAttributes);
508
+
509
+ const blockStruct = this.addStruct(
510
+ [
511
+ parentType,
512
+ new Scope({
513
+ name: blockTypeName,
514
+ parent: parentType,
515
+ isProvider: parentType.isProvider,
516
+ }),
517
+ ],
518
+ blockAttributes,
519
+ blockType.nesting_mode,
520
+ (blockType.nesting_mode === "list" ||
521
+ blockType.nesting_mode === "set") &&
522
+ blockType.max_items === 1,
523
+ );
524
+
525
+ // define the attribute
526
+ attributes.push(
527
+ attributeForBlockType(
528
+ blockTypeName,
529
+ blockType,
530
+ blockStruct,
531
+ parentType.isProvider,
532
+ parentType,
533
+ ),
534
+ );
535
+ }
536
+
537
+ return attributes;
538
+
539
+ function attributeForBlockType(
540
+ terraformName: string,
541
+ blockType: BlockType,
542
+ struct: Struct,
543
+ isProvider: boolean,
544
+ parent: Scope,
545
+ ): AttributeModel {
546
+ const name = toCamelCase(terraformName);
547
+ let optional: boolean;
548
+ let required: boolean;
549
+
550
+ switch (blockType.nesting_mode) {
551
+ case "single":
552
+ optional = !struct.attributes.some((x) => !x.optional);
553
+ required = !struct.attributes.some((x) => !x.required);
554
+
555
+ // This is for bug #3570 as both optional and required evaluate to false under some circumstances
556
+ // (this then causes the computed block to not be part of assignableAttributes and thus skipped in the generated code)
557
+ // Hence: If both optional and required are false, set optional to true IF at least one
558
+ // attribute in the block has optional = true or required = true, as this would mean that at least something can be set
559
+ // and the block is not all computed.
560
+ if (!optional && !required) {
561
+ optional = struct.attributes.some((x) => x.optional || x.required);
562
+ }
563
+
564
+ return new AttributeModel({
565
+ name,
566
+ terraformName,
567
+ terraformFullName: parent.fullName(terraformName),
568
+ type: new StructAttributeTypeModel(struct),
569
+ description: `${terraformName} block`,
570
+ storageName: `_${name}`,
571
+ optional,
572
+ computed: false,
573
+ provider: isProvider,
574
+ required,
575
+ });
576
+
577
+ case "map":
578
+ return new AttributeModel({
579
+ name,
580
+ terraformName,
581
+ terraformFullName: parent.fullName(terraformName),
582
+ type: new MapAttributeTypeModel(
583
+ new StructAttributeTypeModel(struct),
584
+ ),
585
+ description: `${terraformName} block`,
586
+ storageName: `_${name}`,
587
+ optional: false,
588
+ computed: false,
589
+ provider: isProvider,
590
+ required: false,
591
+ });
592
+
593
+ case "list":
594
+ case "set":
595
+ optional =
596
+ blockType.min_items === undefined ? true : blockType.min_items < 1;
597
+ required =
598
+ blockType.min_items === undefined ? false : blockType.min_items > 0;
599
+ return new AttributeModel({
600
+ name,
601
+ terraformName: terraformName,
602
+ terraformFullName: parent.fullName(terraformName),
603
+ type:
604
+ blockType.nesting_mode === "list"
605
+ ? new ListAttributeTypeModel(
606
+ new StructAttributeTypeModel(struct),
607
+ blockType.max_items === 1,
608
+ true,
609
+ )
610
+ : new SetAttributeTypeModel(
611
+ new StructAttributeTypeModel(struct),
612
+ blockType.max_items === 1,
613
+ true,
614
+ ),
615
+ description: `${terraformName} block`,
616
+ storageName: `_${name}`,
617
+ optional,
618
+ computed: false,
619
+ provider: isProvider,
620
+ required,
621
+ });
622
+ }
623
+ }
624
+ }
625
+ private addAnonymousStruct(
626
+ scope: Scope[],
627
+ attrs: { [name: string]: Attribute } | undefined,
628
+ nesting_mode: string,
629
+ ) {
630
+ let attributes = new Array<AttributeModel>();
631
+ const parent = scope[scope.length - 1];
632
+ if (attrs) {
633
+ for (const [terraformName, att] of Object.entries(attrs || {})) {
634
+ // nested types support computed, optional and required on attribute level
635
+ // if parent is computed, child always is computed as well
636
+ const computed =
637
+ !!parent.isComputed || (parent.isNestedType && !!att.computed);
638
+ const optional = parent.isNestedType
639
+ ? !!att.optional
640
+ : !!parent.isOptional;
641
+ const required = parent.isNestedType
642
+ ? !!att.required
643
+ : !!parent.isRequired;
644
+ const name = toCamelCase(terraformName);
645
+ const type = this.renderAttributeType(
646
+ [
647
+ ...scope,
648
+ new Scope({
649
+ name: terraformName,
650
+ parent,
651
+ isProvider: parent.isProvider,
652
+ isComputed: computed,
653
+ isOptional: optional,
654
+ isRequired: required,
655
+ isNestedType: parent.isNestedType,
656
+ }),
657
+ ],
658
+ att.type || att.nested_type,
659
+ );
660
+ attributes.push(
661
+ new AttributeModel({
662
+ name,
663
+ storageName: `_${name}`,
664
+ computed: computed,
665
+ description: att.description,
666
+ optional: optional,
667
+ terraformName,
668
+ terraformFullName: parent.fullName(terraformName),
669
+ type,
670
+ provider: parent.isProvider,
671
+ required: required,
672
+ }),
673
+ );
674
+ }
675
+ }
676
+
677
+ attributes = deduplicateAttributesWithSameName(attributes);
678
+
679
+ return this.addStruct(scope, attributes, nesting_mode);
680
+ }
681
+
682
+ private addStruct(
683
+ scope: Scope[],
684
+ attributes: AttributeModel[],
685
+ nesting_mode: string,
686
+ isSingleItem = false,
687
+ ) {
688
+ const possibleName = toPascalCase(
689
+ scope.map((x) => toSnakeCase(x.name)).join("_"),
690
+ );
691
+ const name = this.uniqueClassName(
692
+ isReservedStructClassName(possibleName)
693
+ ? `${possibleName}Struct`
694
+ : possibleName,
695
+ );
696
+
697
+ const parent = scope[scope.length - 1];
698
+ // blockType.nesting_mode => list/set & blockType.max_items === 1,
699
+ const isClass = (parent.isComputed && !parent.isOptional) || isSingleItem;
700
+ const isAnonymous = true;
701
+ const s = new Struct(
702
+ name,
703
+ attributes,
704
+ isClass,
705
+ isAnonymous,
706
+ isSingleItem,
707
+ nesting_mode,
708
+ );
709
+ this.structs.push(s);
710
+ return s;
711
+ }
712
+ }
713
+
714
+ export class ResourceParser {
715
+ private uniqueClassnames: string[] = [];
716
+ private uniqueBaseNames: Map<string, string> = new Map();
717
+ private resources: Record<string, ResourceModel> = {};
718
+
719
+ public parse(
720
+ fqpn: FQPN,
721
+ type: string,
722
+ schema: Schema,
723
+ terraformType: string,
724
+ constraint?: ConstructsMakerTarget,
725
+ ): ResourceModel {
726
+ if (this.resources[type]) {
727
+ return this.resources[type];
728
+ }
729
+
730
+ const parser = new Parser(this.uniqueClassnames, this.uniqueBaseNames);
731
+ const resource = parser.resourceFrom(
732
+ fqpn,
733
+ type,
734
+ schema,
735
+ terraformType,
736
+ constraint,
737
+ );
738
+ this.resources[type] = resource;
739
+ return resource;
740
+ }
741
+
742
+ // Used by convert to determine the right name for a class
743
+ public getClassNameForResource(terraformType: string) {
744
+ const resource = this.resources[terraformType];
745
+ return resource ? resource.className : "";
746
+ }
747
+
748
+ // Used by convert to determine the right name for a namespace
749
+ public getNamespaceNameForResource(terraformType: string) {
750
+ // Special case external provider since the name of resource doesn't have a prefix
751
+ if (terraformType === "data_external_") {
752
+ terraformType = "data_external";
753
+ }
754
+
755
+ const resource = this.resources[terraformType];
756
+ if (!resource) {
757
+ return "";
758
+ }
759
+ const folder = `providers/${resource.provider}`;
760
+ return resource.filePath.replace(`${folder}/`, "").replace("/index.ts", "");
761
+ }
762
+ }