@aws/nx-plugin 1.0.0-rc.30 → 1.0.0-rc.31

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 (47) hide show
  1. package/LICENSE-THIRD-PARTY +235 -1336
  2. package/README.md +1 -1
  3. package/package.json +1 -2
  4. package/src/mcp-server/tools/create-workspace-command.js +2 -2
  5. package/src/mcp-server/tools/create-workspace-command.js.map +1 -1
  6. package/src/open-api/ts-client/__snapshots__/generator.additional-properties.spec.ts.snap +147 -33
  7. package/src/open-api/ts-client/__snapshots__/generator.arrays.spec.ts.snap +308 -75
  8. package/src/open-api/ts-client/__snapshots__/generator.complex-types.spec.ts.snap +286 -62
  9. package/src/open-api/ts-client/__snapshots__/generator.composite-types.spec.ts.snap +674 -93
  10. package/src/open-api/ts-client/__snapshots__/generator.content-type.spec.ts.snap +147 -33
  11. package/src/open-api/ts-client/__snapshots__/generator.duplicate-types.spec.ts.snap +196 -44
  12. package/src/open-api/ts-client/__snapshots__/generator.edge-cases.spec.ts.snap +5507 -0
  13. package/src/open-api/ts-client/__snapshots__/generator.fast-api.spec.ts.snap +219 -60
  14. package/src/open-api/ts-client/__snapshots__/generator.primitive-types.spec.ts.snap +351 -80
  15. package/src/open-api/ts-client/__snapshots__/generator.request.spec.ts.snap +170 -49
  16. package/src/open-api/ts-client/__snapshots__/generator.reserved-keywords.spec.ts.snap +98 -22
  17. package/src/open-api/ts-client/__snapshots__/generator.response.spec.ts.snap +147 -33
  18. package/src/open-api/ts-client/__snapshots__/generator.streaming.spec.ts.snap +392 -88
  19. package/src/open-api/ts-client/__snapshots__/generator.tags.spec.ts.snap +196 -44
  20. package/src/open-api/ts-client/files/client.gen.ts.template +130 -18
  21. package/src/open-api/ts-client/files/types.gen.ts.template +4 -4
  22. package/src/open-api/ts-client/generator.js +1 -1
  23. package/src/open-api/ts-client/generator.js.map +1 -1
  24. package/src/open-api/ts-hooks/files/options-proxy.gen.ts.template +5 -0
  25. package/src/open-api/ts-hooks/generator.spec.tsx +70 -0
  26. package/src/open-api/utils/codegen-data/languages.d.ts +0 -6
  27. package/src/open-api/utils/codegen-data/languages.js +23 -18
  28. package/src/open-api/utils/codegen-data/languages.js.map +1 -1
  29. package/src/open-api/utils/codegen-data/types.d.ts +240 -10
  30. package/src/open-api/utils/codegen-data/types.js +24 -1
  31. package/src/open-api/utils/codegen-data/types.js.map +1 -1
  32. package/src/open-api/utils/codegen-data.d.ts +2 -2
  33. package/src/open-api/utils/codegen-data.js +357 -617
  34. package/src/open-api/utils/codegen-data.js.map +1 -1
  35. package/src/open-api/utils/normalise.js +157 -19
  36. package/src/open-api/utils/normalise.js.map +1 -1
  37. package/src/open-api/utils/parser.d.ts +55 -0
  38. package/src/open-api/utils/parser.js +743 -0
  39. package/src/open-api/utils/parser.js.map +1 -0
  40. package/src/open-api/utils/types.d.ts +15 -0
  41. package/src/open-api/utils/types.js.map +1 -1
  42. package/src/preset/__snapshots__/generator.spec.ts.snap +6 -6
  43. package/src/utils/mcp.js +1 -1
  44. package/src/utils/mcp.js.map +1 -1
  45. package/src/utils/names.d.ts +6 -0
  46. package/src/utils/names.js +6 -1
  47. package/src/utils/names.js.map +1 -1
@@ -11,7 +11,7 @@ exports[`openApiTsClientGenerator - tags > should allow duplicate operation ids
11
11
  * Utility for serialisation and deserialisation of API types.
12
12
  */
13
13
  export class $IO {
14
- protected static $mapValues = (data: any, fn: (item: any) => any) =>
14
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
15
15
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
16
16
  }
17
17
 
@@ -54,11 +54,31 @@ export class TestApi {
54
54
  this._usersPeopleList = this._usersPeopleList.bind(this);
55
55
  }
56
56
 
57
+ private $collectionDelimiters: { [format: string]: string } = {
58
+ csv: ',',
59
+ ssv: ' ',
60
+ pipes: '|',
61
+ };
62
+
63
+ private $deepObject = (key: string, value: any): string[] => {
64
+ if (value === undefined || value === null) {
65
+ return [];
66
+ }
67
+ if (typeof value !== 'object') {
68
+ return [\`\${key}=\${encodeURIComponent(String(value))}\`];
69
+ }
70
+ return Object.entries(value).flatMap(([prop, v]) =>
71
+ this.$deepObject(\`\${key}[\${encodeURIComponent(prop)}]\`, v),
72
+ );
73
+ };
74
+
57
75
  private $url = (
58
76
  path: string,
59
77
  pathParameters: { [key: string]: any },
60
78
  queryParameters: { [key: string]: any },
61
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
79
+ collectionFormats?: {
80
+ [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject';
81
+ },
62
82
  ): string => {
63
83
  const baseUrl = this.$config.url.endsWith('/')
64
84
  ? this.$config.url.slice(0, -1)
@@ -69,15 +89,24 @@ export class TestApi {
69
89
  path,
70
90
  );
71
91
  const queryString = Object.entries(queryParameters)
72
- .map(([key, value]) => {
92
+ .flatMap(([key, value]) => {
73
93
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
74
- return value
75
- .map(
76
- (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
77
- )
78
- .join('&');
94
+ return value.map(
95
+ (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
96
+ );
97
+ }
98
+ if (
99
+ collectionFormats?.[key] === 'deepObject' &&
100
+ value !== null &&
101
+ typeof value === 'object'
102
+ ) {
103
+ return this.$deepObject(encodeURIComponent(key), value);
79
104
  }
80
- return \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(',') : String(value))}\`;
105
+ const delimiter =
106
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
107
+ return [
108
+ \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}\`,
109
+ ];
81
110
  })
82
111
  .join('&');
83
112
  return (
@@ -87,13 +116,22 @@ export class TestApi {
87
116
 
88
117
  private $headers = (
89
118
  headerParameters: { [key: string]: any },
90
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
119
+ collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' },
91
120
  ): [string, string][] => {
92
121
  return Object.entries(headerParameters).flatMap(([key, value]) => {
93
122
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
94
123
  return value.map((v) => [key, String(v)]) as [string, string][];
95
124
  }
96
- return [[key, String(value)]];
125
+ const delimiter =
126
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
127
+ return [
128
+ [
129
+ key,
130
+ Array.isArray(value)
131
+ ? value.map(String).join(delimiter)
132
+ : String(value),
133
+ ],
134
+ ];
97
135
  });
98
136
  };
99
137
 
@@ -190,7 +228,7 @@ exports[`openApiTsClientGenerator - tags > should allow duplicate operation ids
190
228
  * Utility for serialisation and deserialisation of API types.
191
229
  */
192
230
  export class $IO {
193
- protected static $mapValues = (data: any, fn: (item: any) => any) =>
231
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
194
232
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
195
233
  }
196
234
 
@@ -233,11 +271,31 @@ export class TestApi {
233
271
  this._usersList = this._usersList.bind(this);
234
272
  }
235
273
 
274
+ private $collectionDelimiters: { [format: string]: string } = {
275
+ csv: ',',
276
+ ssv: ' ',
277
+ pipes: '|',
278
+ };
279
+
280
+ private $deepObject = (key: string, value: any): string[] => {
281
+ if (value === undefined || value === null) {
282
+ return [];
283
+ }
284
+ if (typeof value !== 'object') {
285
+ return [\`\${key}=\${encodeURIComponent(String(value))}\`];
286
+ }
287
+ return Object.entries(value).flatMap(([prop, v]) =>
288
+ this.$deepObject(\`\${key}[\${encodeURIComponent(prop)}]\`, v),
289
+ );
290
+ };
291
+
236
292
  private $url = (
237
293
  path: string,
238
294
  pathParameters: { [key: string]: any },
239
295
  queryParameters: { [key: string]: any },
240
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
296
+ collectionFormats?: {
297
+ [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject';
298
+ },
241
299
  ): string => {
242
300
  const baseUrl = this.$config.url.endsWith('/')
243
301
  ? this.$config.url.slice(0, -1)
@@ -248,15 +306,24 @@ export class TestApi {
248
306
  path,
249
307
  );
250
308
  const queryString = Object.entries(queryParameters)
251
- .map(([key, value]) => {
309
+ .flatMap(([key, value]) => {
252
310
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
253
- return value
254
- .map(
255
- (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
256
- )
257
- .join('&');
311
+ return value.map(
312
+ (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
313
+ );
314
+ }
315
+ if (
316
+ collectionFormats?.[key] === 'deepObject' &&
317
+ value !== null &&
318
+ typeof value === 'object'
319
+ ) {
320
+ return this.$deepObject(encodeURIComponent(key), value);
258
321
  }
259
- return \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(',') : String(value))}\`;
322
+ const delimiter =
323
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
324
+ return [
325
+ \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}\`,
326
+ ];
260
327
  })
261
328
  .join('&');
262
329
  return (
@@ -266,13 +333,22 @@ export class TestApi {
266
333
 
267
334
  private $headers = (
268
335
  headerParameters: { [key: string]: any },
269
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
336
+ collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' },
270
337
  ): [string, string][] => {
271
338
  return Object.entries(headerParameters).flatMap(([key, value]) => {
272
339
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
273
340
  return value.map((v) => [key, String(v)]) as [string, string][];
274
341
  }
275
- return [[key, String(value)]];
342
+ const delimiter =
343
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
344
+ return [
345
+ [
346
+ key,
347
+ Array.isArray(value)
348
+ ? value.map(String).join(delimiter)
349
+ : String(value),
350
+ ],
351
+ ];
276
352
  });
277
353
  };
278
354
 
@@ -357,7 +433,7 @@ exports[`openApiTsClientGenerator - tags > should handle operation tags and mult
357
433
  * Utility for serialisation and deserialisation of API types.
358
434
  */
359
435
  export class $IO {
360
- protected static $mapValues = (data: any, fn: (item: any) => any) =>
436
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
361
437
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
362
438
  }
363
439
 
@@ -402,11 +478,31 @@ export class TestApi {
402
478
  this._getUsers = this._getUsers.bind(this);
403
479
  }
404
480
 
481
+ private $collectionDelimiters: { [format: string]: string } = {
482
+ csv: ',',
483
+ ssv: ' ',
484
+ pipes: '|',
485
+ };
486
+
487
+ private $deepObject = (key: string, value: any): string[] => {
488
+ if (value === undefined || value === null) {
489
+ return [];
490
+ }
491
+ if (typeof value !== 'object') {
492
+ return [\`\${key}=\${encodeURIComponent(String(value))}\`];
493
+ }
494
+ return Object.entries(value).flatMap(([prop, v]) =>
495
+ this.$deepObject(\`\${key}[\${encodeURIComponent(prop)}]\`, v),
496
+ );
497
+ };
498
+
405
499
  private $url = (
406
500
  path: string,
407
501
  pathParameters: { [key: string]: any },
408
502
  queryParameters: { [key: string]: any },
409
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
503
+ collectionFormats?: {
504
+ [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject';
505
+ },
410
506
  ): string => {
411
507
  const baseUrl = this.$config.url.endsWith('/')
412
508
  ? this.$config.url.slice(0, -1)
@@ -417,15 +513,24 @@ export class TestApi {
417
513
  path,
418
514
  );
419
515
  const queryString = Object.entries(queryParameters)
420
- .map(([key, value]) => {
516
+ .flatMap(([key, value]) => {
421
517
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
422
- return value
423
- .map(
424
- (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
425
- )
426
- .join('&');
518
+ return value.map(
519
+ (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
520
+ );
521
+ }
522
+ if (
523
+ collectionFormats?.[key] === 'deepObject' &&
524
+ value !== null &&
525
+ typeof value === 'object'
526
+ ) {
527
+ return this.$deepObject(encodeURIComponent(key), value);
427
528
  }
428
- return \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(',') : String(value))}\`;
529
+ const delimiter =
530
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
531
+ return [
532
+ \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}\`,
533
+ ];
429
534
  })
430
535
  .join('&');
431
536
  return (
@@ -435,13 +540,22 @@ export class TestApi {
435
540
 
436
541
  private $headers = (
437
542
  headerParameters: { [key: string]: any },
438
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
543
+ collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' },
439
544
  ): [string, string][] => {
440
545
  return Object.entries(headerParameters).flatMap(([key, value]) => {
441
546
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
442
547
  return value.map((v) => [key, String(v)]) as [string, string][];
443
548
  }
444
- return [[key, String(value)]];
549
+ const delimiter =
550
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
551
+ return [
552
+ [
553
+ key,
554
+ Array.isArray(value)
555
+ ? value.map(String).join(delimiter)
556
+ : String(value),
557
+ ],
558
+ ];
445
559
  });
446
560
  };
447
561
 
@@ -579,7 +693,7 @@ exports[`openApiTsClientGenerator - tags > should handle operations with multipl
579
693
  * Utility for serialisation and deserialisation of API types.
580
694
  */
581
695
  export class $IO {
582
- protected static $mapValues = (data: any, fn: (item: any) => any) =>
696
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
583
697
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
584
698
  }
585
699
 
@@ -622,11 +736,31 @@ export class TestApi {
622
736
  this._postMultiTagged = this._postMultiTagged.bind(this);
623
737
  }
624
738
 
739
+ private $collectionDelimiters: { [format: string]: string } = {
740
+ csv: ',',
741
+ ssv: ' ',
742
+ pipes: '|',
743
+ };
744
+
745
+ private $deepObject = (key: string, value: any): string[] => {
746
+ if (value === undefined || value === null) {
747
+ return [];
748
+ }
749
+ if (typeof value !== 'object') {
750
+ return [\`\${key}=\${encodeURIComponent(String(value))}\`];
751
+ }
752
+ return Object.entries(value).flatMap(([prop, v]) =>
753
+ this.$deepObject(\`\${key}[\${encodeURIComponent(prop)}]\`, v),
754
+ );
755
+ };
756
+
625
757
  private $url = (
626
758
  path: string,
627
759
  pathParameters: { [key: string]: any },
628
760
  queryParameters: { [key: string]: any },
629
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
761
+ collectionFormats?: {
762
+ [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject';
763
+ },
630
764
  ): string => {
631
765
  const baseUrl = this.$config.url.endsWith('/')
632
766
  ? this.$config.url.slice(0, -1)
@@ -637,15 +771,24 @@ export class TestApi {
637
771
  path,
638
772
  );
639
773
  const queryString = Object.entries(queryParameters)
640
- .map(([key, value]) => {
774
+ .flatMap(([key, value]) => {
641
775
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
642
- return value
643
- .map(
644
- (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
645
- )
646
- .join('&');
776
+ return value.map(
777
+ (v) => \`\${encodeURIComponent(key)}=\${encodeURIComponent(\`\${v}\`)}\`,
778
+ );
779
+ }
780
+ if (
781
+ collectionFormats?.[key] === 'deepObject' &&
782
+ value !== null &&
783
+ typeof value === 'object'
784
+ ) {
785
+ return this.$deepObject(encodeURIComponent(key), value);
647
786
  }
648
- return \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(',') : String(value))}\`;
787
+ const delimiter =
788
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
789
+ return [
790
+ \`\${encodeURIComponent(key)}=\${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}\`,
791
+ ];
649
792
  })
650
793
  .join('&');
651
794
  return (
@@ -655,13 +798,22 @@ export class TestApi {
655
798
 
656
799
  private $headers = (
657
800
  headerParameters: { [key: string]: any },
658
- collectionFormats?: { [key: string]: 'multi' | 'csv' },
801
+ collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' },
659
802
  ): [string, string][] => {
660
803
  return Object.entries(headerParameters).flatMap(([key, value]) => {
661
804
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
662
805
  return value.map((v) => [key, String(v)]) as [string, string][];
663
806
  }
664
- return [[key, String(value)]];
807
+ const delimiter =
808
+ this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
809
+ return [
810
+ [
811
+ key,
812
+ Array.isArray(value)
813
+ ? value.map(String).join(delimiter)
814
+ : String(value),
815
+ ],
816
+ ];
665
817
  });
666
818
  };
667
819
 
@@ -16,7 +16,19 @@ const uniqStrings = (strings) => {
16
16
  return true;
17
17
  });
18
18
  };
19
- const returnTypeSet = new Set(allOperations.map(op => op.result.typescriptType));
19
+ const returnTypeSet = new Set(allOperations.flatMap(op => op.result ? [op.result.typescriptType] : []));
20
+ // Names of discriminator "base" models (an object schema whose subtypes allOf-
21
+ // compose it). A subtype composes its base's non-dispatching `$toJsonBase`/
22
+ // `$fromJsonBase` body, so composing the dispatching `toJson`/`fromJson` would
23
+ // recurse infinitely (base -> subtype -> base).
24
+ const discriminatorBaseNames = new Set(models.filter(m => m.discriminator && m.discriminator.isBase).map(m => m.name));
25
+ const modelByName = Object.fromEntries(models.map(m => [m.name, m]));
26
+ // The $IO marshaller call for a composed member: a subtype composes its base's
27
+ // non-dispatching body to avoid recursing back through the base's dispatch.
28
+ const composedMarshaller = (property, method) =>
29
+ property.export === 'reference' && discriminatorBaseNames.has(property.type)
30
+ ? `$IO.${property.typescriptType}.$${method}Base`
31
+ : `$IO.${property.typescriptType}.${method}`;
20
32
  _%>
21
33
  <%_ if ((models.length + allOperations.filter(p => p.parameters.length > 0).length) > 0) { _%>
22
34
  import type {
@@ -44,14 +56,18 @@ const renderToJsonDateValue = (identifier, format) => {
44
56
  // Renders the appropriate nested function for .map() or $mapValues() for arrays and dictionaries for the given type
45
57
  const renderNestedToJsonValue = (type, argumentType = undefined, depth = 0) => {
46
58
  const itemIdentifier = `item${depth}`;
47
- const argType = `${argumentType ? `: ${argumentType}` : ''}`;
59
+ // Nested items (depth > 0) come from mapping over an `any`, so annotate them
60
+ // to satisfy noImplicitAny.
61
+ const argType = `${argumentType ? `: ${argumentType}` : (depth > 0 ? ': any' : '')}`;
48
62
  if (type.isPrimitive || type.typescriptType === 'unknown' || type.export === 'enum' || type.isEnum) {
49
63
  return `(${itemIdentifier}${argType}) => ${["date", "date-time"].includes(type.format) ? renderToJsonDateValue(itemIdentifier, type.format) : itemIdentifier}`;
50
64
  } else if (type.export === "array") {
51
65
  return `(${itemIdentifier}${argType}) => ${itemIdentifier}.map(${renderNestedToJsonValue(type.link, argumentType, depth + 1)})`;
52
66
  } else if (type.export === "dictionary") {
53
67
  return `(${itemIdentifier}${argType}) => $IO.$mapValues(${itemIdentifier}, ${renderNestedToJsonValue(type.link, argumentType, depth + 1)})`;
54
- } else if (type.type === 'unknown') {
68
+ } else if (type.type === 'unknown' && !["one-of", "any-of", "all-of"].includes(type.export)) {
69
+ // A bare `unknown` element passes through; a composite has type `unknown`
70
+ // but must still be marshalled via its own $IO entry.
55
71
  return `(${itemIdentifier}${argType}) => ${itemIdentifier}`;
56
72
  }
57
73
  return `$IO.${type.typescriptType || type.typescriptName}.toJson`;
@@ -83,14 +99,18 @@ const renderToJsonValue = (property, value, argumentType = undefined) => {
83
99
  // Renders the appropriate nested function for .map() or $mapValues() for arrays and dictionaries for the given type
84
100
  const renderNestedFromJsonValue = (type, argumentType = undefined, depth = 0) => {
85
101
  const itemIdentifier = `item${depth}`;
86
- const argType = `${argumentType ? `: ${argumentType}` : ''}`;
102
+ // Nested items (depth > 0) come from mapping over an `any`, so annotate them
103
+ // to satisfy noImplicitAny.
104
+ const argType = `${argumentType ? `: ${argumentType}` : (depth > 0 ? ': any' : '')}`;
87
105
  if (type.isPrimitive || type.typescriptType === 'unknown' || type.export === 'enum' || type.isEnum) {
88
106
  return `(${itemIdentifier}${argType}) => ${["date", "date-time"].includes(type.format) ? `new Date(${itemIdentifier})` : itemIdentifier}`;
89
107
  } else if (type.export === "array") {
90
108
  return `(${itemIdentifier}${argType}) => ${itemIdentifier}.map(${renderNestedFromJsonValue(type.link, argumentType, depth + 1)})`;
91
109
  } else if (type.export === "dictionary") {
92
110
  return `(${itemIdentifier}${argType}) => $IO.$mapValues(${itemIdentifier}, ${renderNestedFromJsonValue(type.link, argumentType, depth + 1)})`;
93
- } else if (type.type === 'unknown') {
111
+ } else if (type.type === 'unknown' && !["one-of", "any-of", "all-of"].includes(type.export)) {
112
+ // A bare `unknown` element passes through; a composite has type `unknown`
113
+ // but must still be marshalled via its own $IO entry.
94
114
  return `(${itemIdentifier}${argType}) => ${itemIdentifier}`;
95
115
  }
96
116
  return `$IO.${type.typescriptType || type.typescriptName}.fromJson`;
@@ -163,13 +183,44 @@ _%>
163
183
  * Utility for serialisation and deserialisation of API types.
164
184
  */
165
185
  export class $IO {
166
- protected static $mapValues = (data: any, fn: (item: any) => any) =>
186
+ public static $mapValues = (data: any, fn: (item: any) => any) =>
167
187
  Object.fromEntries(Object.entries(data).map(([k, v]) => [k, fn(v)]));
168
188
  <%_ models.filter(m => m.export !== 'enum').forEach((model) => { _%>
169
189
  <%_ const isComposite = model.export === "one-of" || model.export === "any-of" || model.export === "all-of"; _%>
190
+ <%_ /* A discriminator base emits its plain (own-fields) body under a $-prefixed
191
+ name that its subtypes compose, while its public toJson/fromJson dispatch
192
+ to the matching subtype — so composing a subtype never recurses back into
193
+ the dispatch. */ _%>
194
+ <%_ const isBase = model.discriminator && model.discriminator.isBase; _%>
195
+ <%_ const toJsonName = isBase ? '$toJsonBase' : 'toJson'; _%>
196
+ <%_ const fromJsonName = isBase ? '$fromJsonBase' : 'fromJson'; _%>
170
197
 
171
198
  public static <%- model.typescriptName %> = {
199
+ <%_ if (isBase) { _%>
172
200
  toJson: (model: <%- model.typescriptName %>): any => {
201
+ if (model === undefined || model === null) {
202
+ return model;
203
+ }
204
+ switch ((model as any).<%- model.discriminator.typescriptPropertyName %>) {
205
+ <%_ model.discriminator.mapping.filter(m => modelByName[m.modelName]).forEach(({ value, modelName }) => { _%>
206
+ case <%- JSON.stringify(value) %>: return $IO.<%- modelByName[modelName].typescriptName %>.toJson(model as <%- modelByName[modelName].typescriptName %>);
207
+ <%_ }); _%>
208
+ }
209
+ return $IO.<%- model.typescriptName %>.$toJsonBase(model);
210
+ },
211
+ fromJson: (json: any): <%- model.typescriptName %> => {
212
+ if (json === undefined || json === null) {
213
+ return json;
214
+ }
215
+ switch (json?.[<%- JSON.stringify(model.discriminator.propertyName) %>]) {
216
+ <%_ model.discriminator.mapping.filter(m => modelByName[m.modelName]).forEach(({ value, modelName }) => { _%>
217
+ case <%- JSON.stringify(value) %>: return $IO.<%- modelByName[modelName].typescriptName %>.fromJson(json);
218
+ <%_ }); _%>
219
+ }
220
+ return $IO.<%- model.typescriptName %>.$fromJsonBase(json);
221
+ },
222
+ <%_ } _%>
223
+ <%- toJsonName %>: (model: <%- model.typescriptName %>): any => {
173
224
  if (model === undefined || model === null) {
174
225
  return model;
175
226
  }
@@ -202,10 +253,25 @@ export class $IO {
202
253
  return <%- renderToJsonValue(arrayOfNonPrimitives, 'model') %>;
203
254
  }
204
255
  <%_ } _%>
256
+ <%_ /* A discriminated composite marshals directly to the matching branch,
257
+ rather than merging every branch (which would leak fields from the
258
+ non-matching branches). Unknown values fall through to the merge. */ _%>
259
+ <%_ if (model.discriminator) { _%>
260
+ <%_ const discToJsonByName = Object.fromEntries(model.composedModels.map(m => [m.name, m.typescriptName])); _%>
261
+ switch ((model as any).<%- model.discriminator.typescriptPropertyName %>) {
262
+ <%_ model.discriminator.mapping.filter(m => discToJsonByName[m.modelName]).forEach(({ value, modelName }) => { _%>
263
+ case <%- JSON.stringify(value) %>: return $IO.<%- discToJsonByName[modelName] %>.toJson(model as <%- discToJsonByName[modelName] %>);
264
+ <%_ }); _%>
265
+ }
266
+ <%_ } _%>
205
267
  <%_ if (model.composedModels.length > 0) { _%>
206
268
  return {
207
269
  <%_ model.properties.filter(p => !p.isPrimitive && p.export !== 'array').forEach((property, i) => { _%>
270
+ <%_ if (property.export === 'reference' && discriminatorBaseNames.has(property.type)) { _%>
271
+ ...<%- composedMarshaller(property, 'toJson') %>(model),
272
+ <%_ } else { _%>
208
273
  ...<%- renderToJsonValue(property, `model${(model.export !== 'all-of' && property.export !== 'dictionary') ? ` as ${property.typescriptType}` : ''}`) %>,
274
+ <%_ } _%>
209
275
  <%_ }); _%>
210
276
  };
211
277
  <%_ } else { _%>
@@ -238,7 +304,7 @@ export class $IO {
238
304
  };
239
305
  <%_ } _%>
240
306
  },
241
- fromJson: (json: any): <%- model.typescriptName %> => {
307
+ <%- fromJsonName %>: (json: any): <%- model.typescriptName %> => {
242
308
  if (json === undefined || json === null) {
243
309
  return json;
244
310
  }
@@ -270,10 +336,25 @@ export class $IO {
270
336
  return <%- renderFromJsonValue(arrayOfNonPrimitives, 'json') %>;
271
337
  }
272
338
  <%_ } _%>
339
+ <%_ /* A discriminated composite marshals directly to the matching branch;
340
+ the discriminator value comes off the raw json by its wire name.
341
+ Unknown values fall through to the merge. */ _%>
342
+ <%_ if (model.discriminator) { _%>
343
+ <%_ const discFromJsonByName = Object.fromEntries(model.composedModels.map(m => [m.name, m.typescriptName])); _%>
344
+ switch (json?.[<%- JSON.stringify(model.discriminator.propertyName) %>]) {
345
+ <%_ model.discriminator.mapping.filter(m => discFromJsonByName[m.modelName]).forEach(({ value, modelName }) => { _%>
346
+ case <%- JSON.stringify(value) %>: return $IO.<%- discFromJsonByName[modelName] %>.fromJson(json);
347
+ <%_ }); _%>
348
+ }
349
+ <%_ } _%>
273
350
  <%_ if (model.composedModels.length > 0) { _%>
274
351
  return {
275
352
  <%_ model.properties.filter(p => !p.isPrimitive && p.export !== 'array').forEach((composedType) => { _%>
353
+ <%_ if (composedType.export === 'reference' && discriminatorBaseNames.has(composedType.type)) { _%>
354
+ ...<%- composedMarshaller(composedType, 'fromJson') %>(json),
355
+ <%_ } else { _%>
276
356
  ...<%- renderFromJsonValue(composedType, 'json') %>,
357
+ <%_ } _%>
277
358
  <%_ }); _%>
278
359
  };
279
360
  <%_ } else { _%>
@@ -355,26 +436,45 @@ export class <%- className %> {
355
436
  <%_ }); _%>
356
437
  }
357
438
 
358
- private $url = (path: string, pathParameters: { [key: string]: any }, queryParameters: { [key: string]: any }, collectionFormats?: { [key: string]: 'multi' | 'csv' }): string => {
439
+ private $collectionDelimiters: { [format: string]: string } = { csv: ',', ssv: ' ', pipes: '|' };
440
+
441
+ private $deepObject = (key: string, value: any): string[] => {
442
+ if (value === undefined || value === null) {
443
+ return [];
444
+ }
445
+ if (typeof value !== 'object') {
446
+ return [`${key}=${encodeURIComponent(String(value))}`];
447
+ }
448
+ return Object.entries(value).flatMap(([prop, v]) =>
449
+ this.$deepObject(`${key}[${encodeURIComponent(prop)}]`, v),
450
+ );
451
+ };
452
+
453
+ private $url = (path: string, pathParameters: { [key: string]: any }, queryParameters: { [key: string]: any }, collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' | 'deepObject' }): string => {
359
454
  const baseUrl = this.$config.url.endsWith('/') ? this.$config.url.slice(0, -1) : this.$config.url;
360
455
  const pathWithParameters = Object.entries(pathParameters).reduce((withParams, [key, value]) =>
361
456
  withParams.replace(`{${key}}`, encodeURIComponent(`${value}`))
362
457
  , path);
363
- const queryString = Object.entries(queryParameters).map(([key, value]) => {
458
+ const queryString = Object.entries(queryParameters).flatMap(([key, value]) => {
364
459
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
365
- return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(`${v}`)}`).join('&');
460
+ return value.map(v => `${encodeURIComponent(key)}=${encodeURIComponent(`${v}`)}`);
461
+ }
462
+ if (collectionFormats?.[key] === 'deepObject' && value !== null && typeof value === 'object') {
463
+ return this.$deepObject(encodeURIComponent(key), value);
366
464
  }
367
- return `${encodeURIComponent(key)}=${encodeURIComponent(Array.isArray(value) ? value.map(String).join(',') : String(value))}`;
465
+ const delimiter = this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
466
+ return [`${encodeURIComponent(key)}=${encodeURIComponent(Array.isArray(value) ? value.map(String).join(delimiter) : String(value))}`];
368
467
  }).join('&');
369
468
  return baseUrl + pathWithParameters + (queryString ? `?${queryString}` : '');
370
469
  };
371
470
 
372
- private $headers = (headerParameters: { [key: string]: any }, collectionFormats?: { [key: string]: 'multi' | 'csv' }): [string, string][] => {
471
+ private $headers = (headerParameters: { [key: string]: any }, collectionFormats?: { [key: string]: 'multi' | 'csv' | 'ssv' | 'pipes' }): [string, string][] => {
373
472
  return Object.entries(headerParameters).flatMap(([key, value]) => {
374
473
  if (Array.isArray(value) && collectionFormats?.[key] === 'multi') {
375
474
  return value.map(v => [key, String(v)]) as [string, string][];
376
475
  }
377
- return [[key, String(value)]];
476
+ const delimiter = this.$collectionDelimiters[collectionFormats?.[key] ?? 'csv'] ?? ',';
477
+ return [[key, Array.isArray(value) ? value.map(String).join(delimiter) : String(value)]];
378
478
  });
379
479
  };
380
480
 
@@ -418,9 +518,21 @@ export class <%- className %> {
418
518
  }
419
519
  <%_ } _%>
420
520
  <%_ } _%>
421
- <%_ if (op.parameters.filter(p => p.collectionFormat).length > 0) { _%>
422
- const collectionFormats = {
423
- <%_ op.parameters.filter(p => p.collectionFormat).forEach((parameter) => { _%>
521
+ <%_ /* Query and header collection formats are kept in separate maps: a
522
+ query and a header parameter may share a name, which would collide
523
+ in a single object literal. */ _%>
524
+ <%_ const queryCollectionParams = op.parameters.filter(p => p.collectionFormat && p.in === 'query'); _%>
525
+ <%_ const headerCollectionParams = op.parameters.filter(p => p.collectionFormat && p.in === 'header'); _%>
526
+ <%_ if (queryCollectionParams.length > 0) { _%>
527
+ const queryCollectionFormats = {
528
+ <%_ queryCollectionParams.forEach((parameter) => { _%>
529
+ '<%- parameter.prop %>': '<%- parameter.collectionFormat %>',
530
+ <%_ }); _%>
531
+ } as const;
532
+ <%_ } _%>
533
+ <%_ if (headerCollectionParams.length > 0) { _%>
534
+ const headerCollectionFormats = {
535
+ <%_ headerCollectionParams.forEach((parameter) => { _%>
424
536
  '<%- parameter.prop %>': '<%- parameter.collectionFormat %>',
425
537
  <%_ }); _%>
426
538
  } as const;
@@ -457,8 +569,8 @@ export class <%- className %> {
457
569
  const body = undefined;
458
570
  <%_ } _%>
459
571
 
460
- const response = await this.$fetch(this.$url('<%- op.path %>', pathParameters, queryParameters<% if (op.parameters.filter(p => p.collectionFormat).length > 0) { %>, collectionFormats<% } %>), {
461
- headers: this.$headers(headerParameters<% if (op.parameters.filter(p => p.collectionFormat).length > 0) { %>, collectionFormats<% } %>),
572
+ const response = await this.$fetch(this.$url('<%- op.path %>', pathParameters, queryParameters<% if (queryCollectionParams.length > 0) { %>, queryCollectionFormats<% } %>), {
573
+ headers: this.$headers(headerParameters<% if (headerCollectionParams.length > 0) { %>, headerCollectionFormats<% } %>),
462
574
  method: '<%- op.method %>',
463
575
  body,
464
576
  });
@@ -22,7 +22,8 @@ _%>
22
22
  <%_ if (model.export === "enum") { _%>
23
23
  <%- docString(model, '') %>export type <%= model.typescriptName %> =
24
24
  <%_ model.enum.forEach((enumMember, i) => { _%>
25
- | '<%- enumMember.value %>'
25
+ <%_ /* JSON.stringify escapes quotes, newlines and backslashes in values */ _%>
26
+ | <%- JSON.stringify(`${enumMember.value}`) %>
26
27
  <%_ }); _%>
27
28
  <%_ } else if (isComposite) { _%>
28
29
  <%- docString(model, '') %>export type <%- model.typescriptName %> =<% model.properties.forEach((composedType, i) => { %><% if (i > 0) { %><% if (model.export === "all-of") { %> &<% } else { %> |<% } %><% } %> <%- composedType.typescriptType %><% }); %>;
@@ -30,9 +31,8 @@ _%>
30
31
  <%- docString(model, '') %>export type <%= model.typescriptName %> = <% if (model.isNullable && model.type !== 'null') { %>null | <% } %>{
31
32
  <%_ if (model.export === "dictionary" && model.link) { _%>
32
33
  <%_ /* When the spec says `type: object` with `additionalProperties: true`
33
- (or omits additionalProperties), hey-api sets `model.link` but the
34
- link's typescriptType is empty — fall back to `unknown` to keep the
35
- emitted code valid. */ _%>
34
+ (or omits additionalProperties), the value model has an unknown type —
35
+ fall back to `unknown` to keep the emitted code valid. */ _%>
36
36
  <%- docString(model.link, ' ') %> [key: string]: <%- model.link.typescriptType || 'unknown' %>;
37
37
  <%_ } _%>
38
38
  <%_ model.properties.forEach((property) => { _%>