@cdktn/provider-generator 0.24.0-pre.71 → 0.24.0-pre.73

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.
@@ -1,8 +1,13 @@
1
1
  // Copyright (c) HashiCorp, Inc
2
2
  // SPDX-License-Identifier: MPL-2.0
3
3
  import { CodeMaker } from "codemaker";
4
- import { AttributeModel } from "../models";
4
+ import {
5
+ AttributeModel,
6
+ CollectionAttributeTypeModel,
7
+ describeTerraformType,
8
+ } from "../models";
5
9
  import { CUSTOM_DEFAULTS } from "../custom-defaults";
10
+ import { sanitizedComment } from "../sanitized-comments";
6
11
 
7
12
  function titleCase(value: string) {
8
13
  return value[0].toUpperCase() + value.slice(1);
@@ -34,6 +39,8 @@ export class AttributesEmitter {
34
39
  );
35
40
  }
36
41
 
42
+ this.emitStoredClassBoundaryDoc(att);
43
+
37
44
  switch (getterType._type) {
38
45
  case "plain":
39
46
  this.code.openBlock(`public get ${att.name}()`);
@@ -121,6 +128,35 @@ export class AttributesEmitter {
121
128
  return att.type.getStoredClassInitializer(att.terraformName);
122
129
  }
123
130
 
131
+ // Emits a JSDoc block documenting the Terraform type immediately before getters whose
132
+ // stored class either collapsed a deeper, unsupported nesting into the nearest typed
133
+ // "Any"-wrapper, or fell all the way back to a plain, untyped getter because no stored
134
+ // class (not even a collapsed one) is available. Normal, fully-supported getters get no
135
+ // such doc, to limit snapshot churn.
136
+ private emitStoredClassBoundaryDoc(att: AttributeModel) {
137
+ const boundaryKind = att.storedClassBoundaryKind;
138
+ if (!boundaryKind) {
139
+ return;
140
+ }
141
+
142
+ const comment = sanitizedComment(this.code);
143
+ comment.line(`Terraform type: \`${describeTerraformType(att.type)}\`.`);
144
+
145
+ if (boundaryKind === "collapsed") {
146
+ const collapsedName = (att.type as CollectionAttributeTypeModel)
147
+ .resolvedStoredClass!.name;
148
+ comment.line(
149
+ `This nesting is deeper than the typed wrapper classes in the core cdktn package; it is exposed as \`${collapsedName}\` and values below that boundary are untyped tokens.`,
150
+ );
151
+ } else {
152
+ comment.line(
153
+ `No typed wrapper class for this shape exists in the core cdktn package; the value is returned as an untyped token (IResolvable).`,
154
+ );
155
+ }
156
+
157
+ comment.end();
158
+ }
159
+
124
160
  public determineGetAttCall(att: AttributeModel): string {
125
161
  if (att.isProvider) {
126
162
  return `this.${att.storageName}`;
@@ -1,6 +1,10 @@
1
1
  // Copyright (c) HashiCorp, Inc
2
2
  // SPDX-License-Identifier: MPL-2.0
3
- import { AttributeTypeModel } from "./attribute-type-model";
3
+ import { logger } from "@cdktn/commons";
4
+ import {
5
+ AttributeTypeModel,
6
+ CollectionAttributeTypeModel,
7
+ } from "./attribute-type-model";
4
8
 
5
9
  export type GetterType =
6
10
  | { _type: "plain" }
@@ -69,6 +73,7 @@ export class AttributeModel {
69
73
  public provider: boolean;
70
74
  public required: boolean;
71
75
  public forcePlainGetterType?: boolean;
76
+ private loggedStoredClassUnavailable = false;
72
77
 
73
78
  constructor(options: AttributeModelOptions) {
74
79
  this.storageName = options.storageName;
@@ -125,14 +130,63 @@ export class AttributeModel {
125
130
  !this.isAssignable &&
126
131
  (!this.type.isTokenizable || this.type.typeModelType === "map")
127
132
  ) {
128
- getterType = {
129
- _type: "stored_class",
130
- };
133
+ if (this.type.isStoredClassUnavailable) {
134
+ // Defensive fallback: neither the composed stored class name (e.g.
135
+ // "StringListMapMap") nor a collapsed nearest-Any wrapper (e.g. "AnyMapMap")
136
+ // exists in the core cdktn package for this type. This should be rare - see
137
+ // resolveStoredClassName in supported-stored-classes.ts - so log it loudly to
138
+ // make regressions in the collapse logic visible.
139
+ if (!this.loggedStoredClassUnavailable) {
140
+ this.loggedStoredClassUnavailable = true;
141
+ logger.debug(
142
+ `No stored class (not even a collapsed nearest-Any wrapper) is available for computed attribute "${this.terraformFullName}" (composed storedClassType: "${this.type.storedClassType}"). Falling back to a plain, untyped getter.`,
143
+ );
144
+ }
145
+ } else {
146
+ getterType = {
147
+ _type: "stored_class",
148
+ };
149
+ }
131
150
  }
132
151
 
133
152
  return getterType;
134
153
  }
135
154
 
155
+ // Reports whether this attribute's getter sits at a "stored class boundary" that is
156
+ // worth documenting: "collapsed" when a deeper, unsupported nesting was collapsed into
157
+ // the nearest typed "Any"-wrapper (e.g. "AnyMapMap"), or "fallback" when no stored class
158
+ // at all is available and a plain, untyped getter is emitted instead. Mirrors the same
159
+ // condition tree as getterType above, so it only ever reports a boundary for attributes
160
+ // that actually went through that computed-collection branch - plain getters for
161
+ // ordinary tokenizable types (e.g. list(string)) are never flagged.
162
+ public get storedClassBoundaryKind(): "collapsed" | "fallback" | undefined {
163
+ if (this.forcePlainGetterType || this.isProvider) {
164
+ return undefined;
165
+ }
166
+
167
+ if (this.type.hasReferenceClass) {
168
+ return undefined;
169
+ }
170
+
171
+ if (
172
+ this.computed &&
173
+ !this.isAssignable &&
174
+ (!this.type.isTokenizable || this.type.typeModelType === "map")
175
+ ) {
176
+ if (this.type.isStoredClassUnavailable) {
177
+ return "fallback";
178
+ }
179
+
180
+ const resolvedStoredClass = (this.type as CollectionAttributeTypeModel)
181
+ .resolvedStoredClass;
182
+ if (resolvedStoredClass?.collapsed) {
183
+ return "collapsed";
184
+ }
185
+ }
186
+
187
+ return undefined;
188
+ }
189
+
136
190
  public get isStored(): boolean {
137
191
  return this.isAssignable;
138
192
  }
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: MPL-2.0
3
3
  import { Struct } from "./struct";
4
4
  import { uppercaseFirst, downcaseFirst } from "../../../util";
5
+ import { resolveStoredClassName } from "./supported-stored-classes";
5
6
 
6
7
  export interface AttributeTypeModel {
7
8
  readonly struct?: Struct; // complex object type contained within the type
@@ -15,6 +16,37 @@ export interface AttributeTypeModel {
15
16
  readonly typeModelType: string; // so we don't need to use instanceof
16
17
  readonly hasReferenceClass: boolean; // used to determine if a stored_class getter should be used
17
18
  readonly isTokenizable: boolean; // can the type be represented by a token type
19
+ readonly isStoredClassUnavailable: boolean; // true when no "cdktn.<Name>" stored class (not even a collapsed nearest-Any wrapper) is available for this type
20
+ }
21
+
22
+ // Describes the Terraform type of an attribute as its `list(...)`/`set(...)`/`map(...)`
23
+ // composed notation (matching how Terraform itself describes nested collection types),
24
+ // for use in generated JSDoc. Used only for collapsed/fallback getters (see
25
+ // AttributesEmitter) so it never needs to be exhaustive/perfectly stable for every shape.
26
+ export function describeTerraformType(model: AttributeTypeModel): string {
27
+ switch (model.typeModelType) {
28
+ case "list":
29
+ return `list(${describeTerraformType(
30
+ (model as CollectionAttributeTypeModel).elementType,
31
+ )})`;
32
+ case "set":
33
+ return `set(${describeTerraformType(
34
+ (model as CollectionAttributeTypeModel).elementType,
35
+ )})`;
36
+ case "map":
37
+ return `map(${describeTerraformType(
38
+ (model as CollectionAttributeTypeModel).elementType,
39
+ )})`;
40
+ case "struct":
41
+ return "object";
42
+ default:
43
+ // Covers both SimpleAttributeTypeModel (storedClassType is the Terraform primitive
44
+ // name, e.g. "string" | "number" | "boolean" | "any") and SkippedAttributeTypeModel
45
+ // (storedClassType is always "any").
46
+ return model.storedClassType === "boolean"
47
+ ? "bool"
48
+ : model.storedClassType;
49
+ }
18
50
  }
19
51
 
20
52
  export class SkippedAttributeTypeModel implements AttributeTypeModel {
@@ -64,6 +96,10 @@ export class SkippedAttributeTypeModel implements AttributeTypeModel {
64
96
  get isTokenizable() {
65
97
  return false;
66
98
  }
99
+
100
+ get isStoredClassUnavailable() {
101
+ return false;
102
+ }
67
103
  }
68
104
 
69
105
  export class SimpleAttributeTypeModel implements AttributeTypeModel {
@@ -117,6 +153,10 @@ export class SimpleAttributeTypeModel implements AttributeTypeModel {
117
153
  get isTokenizable() {
118
154
  return true;
119
155
  }
156
+
157
+ get isStoredClassUnavailable() {
158
+ return false;
159
+ }
120
160
  }
121
161
 
122
162
  export class StructAttributeTypeModel implements AttributeTypeModel {
@@ -162,10 +202,21 @@ export class StructAttributeTypeModel implements AttributeTypeModel {
162
202
  get isTokenizable() {
163
203
  return false;
164
204
  }
205
+
206
+ get isStoredClassUnavailable() {
207
+ return false;
208
+ }
165
209
  }
166
210
 
167
211
  export interface CollectionAttributeTypeModel extends AttributeTypeModel {
168
212
  elementType: AttributeTypeModel;
213
+ // Resolves the "cdktn.<name>" stored class to instantiate for this (non-complex)
214
+ // collection type, and whether that resolution collapsed a deeper, unsupported nesting
215
+ // into the nearest typed "Any"-wrapper. undefined only defensively - see
216
+ // resolveStoredClassName in supported-stored-classes.ts.
217
+ readonly resolvedStoredClass:
218
+ | { name: string; collapsed: boolean }
219
+ | undefined;
169
220
  }
170
221
 
171
222
  export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
@@ -198,9 +249,7 @@ export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
198
249
  if (this.isComplex) {
199
250
  return `new ${this.storedClassType}(this, "${name}", false)`;
200
251
  } else {
201
- return `new cdktn.${uppercaseFirst(
202
- this.storedClassType,
203
- )}(this, "${name}", false)`;
252
+ return `new cdktn.${this.resolvedStoredClass!.name}(this, "${name}", false)`;
204
253
  }
205
254
  }
206
255
  }
@@ -209,6 +258,13 @@ export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
209
258
  return `${this.elementType.storedClassType}List`;
210
259
  }
211
260
 
261
+ get resolvedStoredClass() {
262
+ return resolveStoredClassName(
263
+ this.storedClassType,
264
+ this.elementType.typeModelType,
265
+ );
266
+ }
267
+
212
268
  get inputTypeDefinition() {
213
269
  if (this.isSingleItem) {
214
270
  return this.elementType.inputTypeDefinition;
@@ -263,6 +319,34 @@ export class ListAttributeTypeModel implements CollectionAttributeTypeModel {
263
319
  return false;
264
320
  }
265
321
  }
322
+
323
+ // Complex (struct) elements generate their own class in the provider output, so they
324
+ // are always fine. Otherwise a stored class is only emitted if resolvedStoredClass
325
+ // finds one - see getStoredClassInitializer above. Note: hasReferenceClass (which
326
+ // triggers a stored_class getter) is only ever true here when isSingleItem or isComplex
327
+ // is true, and isSingleItem is only set for struct/block nesting (which is also
328
+ // isComplex), so this check only needs to guard the non-complex, non-single-item case.
329
+ //
330
+ // storedClassType is itself built up recursively (elementType.storedClassType is
331
+ // composed the same way for nested collections), so it already fully encodes the
332
+ // entire nested chain in one string (e.g. map(map(list(string))) composes all the way
333
+ // up to "stringListMapMap"). resolvedStoredClass first checks this type's own composed
334
+ // name (e.g. "StringListMapMap") against the fixed set of hand-written core classes; if
335
+ // that name doesn't exist, it falls back to the nearest typed "Any"-wrapper that still
336
+ // encodes this type's own layer and its element's layer (e.g. "AnyMapMap"), collapsing
337
+ // only the unsupported inner shape while preserving typed navigation for the outer
338
+ // layers; only if neither exists does it fall through to the plain, untyped getter (see
339
+ // AttributeModel.getterType). It must NOT also OR in
340
+ // elementType.isStoredClassUnavailable, since an inner element's un-composed name (e.g.
341
+ // "StringList" for a plain list(string) nested inside a map) is never separately
342
+ // instantiated and is irrelevant on its own; ORing it in would produce false positives
343
+ // that regress supported nestings such as map(list(string)) => cdktn.StringListMap.
344
+ get isStoredClassUnavailable() {
345
+ if (this.isComplex) {
346
+ return false;
347
+ }
348
+ return this.resolvedStoredClass === undefined;
349
+ }
266
350
  }
267
351
 
268
352
  export class SetAttributeTypeModel implements CollectionAttributeTypeModel {
@@ -295,9 +379,7 @@ export class SetAttributeTypeModel implements CollectionAttributeTypeModel {
295
379
  if (this.isComplex) {
296
380
  return `new ${this.storedClassType}(this, "${name}", true)`;
297
381
  } else {
298
- return `new cdktn.${uppercaseFirst(
299
- this.storedClassType,
300
- )}(this, "${name}", true)`;
382
+ return `new cdktn.${this.resolvedStoredClass!.name}(this, "${name}", true)`;
301
383
  }
302
384
  }
303
385
  }
@@ -306,6 +388,13 @@ export class SetAttributeTypeModel implements CollectionAttributeTypeModel {
306
388
  return `${this.elementType.storedClassType}List`;
307
389
  }
308
390
 
391
+ get resolvedStoredClass() {
392
+ return resolveStoredClassName(
393
+ this.storedClassType,
394
+ this.elementType.typeModelType,
395
+ );
396
+ }
397
+
309
398
  get inputTypeDefinition() {
310
399
  if (this.isSingleItem) {
311
400
  return this.elementType.inputTypeDefinition;
@@ -365,6 +454,15 @@ export class SetAttributeTypeModel implements CollectionAttributeTypeModel {
365
454
  return false;
366
455
  }
367
456
  }
457
+
458
+ // See comment on ListAttributeTypeModel.isStoredClassUnavailable above - same
459
+ // reasoning applies here.
460
+ get isStoredClassUnavailable() {
461
+ if (this.isComplex) {
462
+ return false;
463
+ }
464
+ return this.resolvedStoredClass === undefined;
465
+ }
368
466
  }
369
467
 
370
468
  export class MapAttributeTypeModel implements CollectionAttributeTypeModel {
@@ -386,9 +484,7 @@ export class MapAttributeTypeModel implements CollectionAttributeTypeModel {
386
484
  if (this.isComplex) {
387
485
  return `new ${this.storedClassType}(this, "${name}")`;
388
486
  } else {
389
- return `new cdktn.${uppercaseFirst(
390
- this.storedClassType,
391
- )}(this, "${name}")`;
487
+ return `new cdktn.${this.resolvedStoredClass!.name}(this, "${name}")`;
392
488
  }
393
489
  }
394
490
 
@@ -396,6 +492,13 @@ export class MapAttributeTypeModel implements CollectionAttributeTypeModel {
396
492
  return `${this.elementType.storedClassType}Map`;
397
493
  }
398
494
 
495
+ get resolvedStoredClass() {
496
+ return resolveStoredClassName(
497
+ this.storedClassType,
498
+ this.elementType.typeModelType,
499
+ );
500
+ }
501
+
399
502
  get inputTypeDefinition() {
400
503
  // map of booleans has token support, but booleans don't
401
504
  if (this.elementType.storedClassType === "boolean") {
@@ -445,4 +548,13 @@ export class MapAttributeTypeModel implements CollectionAttributeTypeModel {
445
548
  return false;
446
549
  }
447
550
  }
551
+
552
+ // See comment on ListAttributeTypeModel.isStoredClassUnavailable above - same
553
+ // reasoning applies here (MapAttributeTypeModel.hasReferenceClass is just isComplex).
554
+ get isStoredClassUnavailable() {
555
+ if (this.isComplex) {
556
+ return false;
557
+ }
558
+ return this.resolvedStoredClass === undefined;
559
+ }
448
560
  }
@@ -0,0 +1,88 @@
1
+ // Copyright (c) HashiCorp, Inc
2
+ // SPDX-License-Identifier: MPL-2.0
3
+ import { uppercaseFirst } from "../../../util";
4
+
5
+ // The core cdktn package (packages/cdktn/src/complex-computed-list.ts) only hand-writes
6
+ // this fixed set of wrapper classes for computed, non-complex collection attributes.
7
+ // Arbitrarily deep nestings of list/set/map (e.g. map(map(list(string)))) compose a
8
+ // storedClassType (e.g. "stringListMapMap") that has no corresponding class. Rather than
9
+ // falling back straight to a plain, untyped getter, resolveStoredClassName below first
10
+ // looks for the nearest typed "Any"-wrapper that still exists in this set, collapsing the
11
+ // unsupported inner shape into an untyped boundary while preserving typed navigation for
12
+ // the outer layers (see https://github.com/open-constructs/cdk-terrain/issues/11).
13
+ const SUPPORTED_STORED_CLASSES = new Set([
14
+ "StringMap",
15
+ "NumberMap",
16
+ "BooleanMap",
17
+ "AnyMap",
18
+ "StringMapMap",
19
+ "NumberMapMap",
20
+ "BooleanMapMap",
21
+ "AnyMapMap",
22
+ "BooleanList",
23
+ "StringListList",
24
+ "NumberListList",
25
+ "BooleanListList",
26
+ "AnyListList",
27
+ "StringMapList",
28
+ "NumberMapList",
29
+ "BooleanMapList",
30
+ "AnyMapList",
31
+ "StringListMap",
32
+ "NumberListMap",
33
+ "BooleanListMap",
34
+ "AnyListMap",
35
+ ]);
36
+
37
+ // Resolves the composed stored class name to use for a collection attribute (list/set/map)
38
+ // whose element is itself a non-complex collection (or simple type).
39
+ //
40
+ // `ownStoredClassType` is this type's own composed storedClassType (e.g. "stringListMapMap"
41
+ // for map(map(list(string)))) and `elementTypeModelType` is the element's typeModelType
42
+ // ("list" | "set" | "map" | "simple" | ...).
43
+ //
44
+ // Resolution order:
45
+ // 1. The own composed name (e.g. "StringListMapMap") - used as-is when it refers to a
46
+ // class that is hand-written in the core package (not collapsed).
47
+ // 2. Otherwise, the nearest typed "Any"-wrapper that still encodes the outer two layers
48
+ // (this type's own layer and its element's layer), e.g. "AnyMapMap" - collapsing the
49
+ // unsupported inner shape into an untyped boundary while preserving typed navigation
50
+ // for the outer layers.
51
+ // 3. Otherwise (defensively; every depth should resolve via 1 or 2), undefined - the
52
+ // caller must fall back to a plain, untyped getter.
53
+ export function resolveStoredClassName(
54
+ ownStoredClassType: string,
55
+ elementTypeModelType: string,
56
+ ): { name: string; collapsed: boolean } | undefined {
57
+ const candidate1 = uppercaseFirst(ownStoredClassType);
58
+ if (SUPPORTED_STORED_CLASSES.has(candidate1)) {
59
+ return { name: candidate1, collapsed: false };
60
+ }
61
+
62
+ let ownLayer: "List" | "Map" | undefined;
63
+ if (ownStoredClassType.endsWith("List")) {
64
+ ownLayer = "List";
65
+ } else if (ownStoredClassType.endsWith("Map")) {
66
+ ownLayer = "Map";
67
+ }
68
+ if (!ownLayer) {
69
+ return undefined;
70
+ }
71
+
72
+ let innerLayer: "List" | "Map" | undefined;
73
+ if (elementTypeModelType === "list" || elementTypeModelType === "set") {
74
+ innerLayer = "List";
75
+ } else if (elementTypeModelType === "map") {
76
+ innerLayer = "Map";
77
+ }
78
+ if (!innerLayer) {
79
+ return undefined;
80
+ }
81
+
82
+ const candidate2 = `Any${innerLayer}${ownLayer}`;
83
+ if (SUPPORTED_STORED_CLASSES.has(candidate2)) {
84
+ return { name: candidate2, collapsed: true };
85
+ }
86
+
87
+ return undefined;
88
+ }