@cdktn/provider-generator 0.24.0-pre.43 → 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.
- package/LICENSE +355 -0
- package/README.md +1 -1
- package/build/__tests__/edge-provider-schema/builder.js +3 -3
- package/build/__tests__/edge-provider-schema/cli.js +15 -20
- package/build/__tests__/edge-provider-schema.test.js +8 -18
- package/build/__tests__/provider.test.js +8 -18
- package/build/get/__tests__/constructs-maker.test.js +8 -18
- package/build/get/__tests__/generator/complex-computed-types.test.js +8 -18
- package/build/get/__tests__/generator/deep-nested-attributes.test.js +8 -18
- package/build/get/__tests__/generator/description-escaping.test.js +8 -18
- package/build/get/__tests__/generator/empty-provider-resources.test.js +8 -18
- package/build/get/__tests__/generator/export-sharding.test.js +8 -18
- package/build/get/__tests__/generator/import-style.test.js +8 -18
- package/build/get/__tests__/generator/module-generator.test.js +8 -18
- package/build/get/__tests__/generator/nested-types.test.js +8 -18
- package/build/get/__tests__/generator/provider.test.js +8 -18
- package/build/get/__tests__/generator/resource-types.test.js +8 -18
- package/build/get/__tests__/generator/skipped-attributes.test.js +8 -18
- package/build/get/__tests__/generator/types.test.js +8 -18
- package/build/get/__tests__/generator/versions-file.test.js +8 -18
- package/build/get/__tests__/util.js +11 -20
- package/build/get/constructs-maker.js +19 -24
- package/build/get/generator/emitter/struct-emitter.js +8 -18
- package/build/get/generator/loop-detection.js +3 -2
- package/build/get/generator/models/attribute-model.js +3 -3
- package/build/get/generator/resource-parser.js +3 -3
- package/build/get/generator/sanitized-comments.js +3 -2
- package/build/get/generator/skipped-attributes.js +3 -2
- package/build/index.js +3 -3
- package/build/util.js +4 -3
- package/package.json +17 -15
- package/package.sh +1 -1
- package/src/__tests__/edge-provider-schema/builder.ts +185 -0
- package/src/__tests__/edge-provider-schema/cli.ts +51 -0
- package/src/__tests__/edge-provider-schema/index.ts +161 -0
- package/src/__tests__/edge-provider-schema.test.ts +24 -0
- package/src/__tests__/provider.test.ts +180 -0
- package/src/get/__tests__/constructs-maker.test.ts +118 -0
- package/src/get/__tests__/generator/complex-computed-types.test.ts +28 -0
- package/src/get/__tests__/generator/deep-nested-attributes.test.ts +56 -0
- package/src/get/__tests__/generator/description-escaping.test.ts +84 -0
- package/src/get/__tests__/generator/empty-provider-resources.test.ts +26 -0
- package/src/get/__tests__/generator/export-sharding.test.ts +169 -0
- package/src/get/__tests__/generator/import-style.test.ts +129 -0
- package/src/get/__tests__/generator/module-generator.test.ts +528 -0
- package/src/get/__tests__/generator/nested-types.test.ts +54 -0
- package/src/get/__tests__/generator/provider.test.ts +51 -0
- package/src/get/__tests__/generator/resource-types.test.ts +118 -0
- package/src/get/__tests__/generator/skipped-attributes.test.ts +72 -0
- package/src/get/__tests__/generator/types.test.ts +611 -0
- package/src/get/__tests__/generator/versions-file.test.ts +72 -0
- package/src/get/__tests__/util.ts +75 -0
- package/src/get/constructs-maker.ts +822 -0
- package/src/get/generator/custom-defaults.ts +493 -0
- package/src/get/generator/emitter/attributes-emitter.ts +225 -0
- package/src/get/generator/emitter/index.ts +5 -0
- package/src/get/generator/emitter/resource-emitter.ts +226 -0
- package/src/get/generator/emitter/struct-emitter.ts +683 -0
- package/src/get/generator/loop-detection.ts +81 -0
- package/src/get/generator/models/attribute-model.ts +216 -0
- package/src/get/generator/models/attribute-type-model.ts +448 -0
- package/src/get/generator/models/index.ts +7 -0
- package/src/get/generator/models/resource-model.ts +161 -0
- package/src/get/generator/models/scope.ts +54 -0
- package/src/get/generator/models/struct.ts +116 -0
- package/src/get/generator/module-generator.ts +234 -0
- package/src/get/generator/provider-generator.ts +355 -0
- package/src/get/generator/resource-parser.ts +762 -0
- package/src/get/generator/sanitized-comments.ts +49 -0
- package/src/get/generator/skipped-attributes.ts +27 -0
- package/src/index.ts +40 -0
- 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
|
+
}
|