@axinom/mosaic-cli 0.18.4 → 0.18.5-rc.0

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,17 +1,39 @@
1
1
  /* eslint-disable no-console */
2
2
 
3
3
  import {
4
- CommonNamingConvention,
5
4
  FormatHelpers,
6
5
  OutputModel,
7
- PropertyType,
6
+ TS_DESCRIPTION_PRESET,
7
+ typeScriptDefaultPropertyKeyConstraints,
8
8
  TypeScriptGenerator,
9
9
  } from '@asyncapi/modelina';
10
- import { AsyncAPIDocument, Message, parse } from '@asyncapi/parser';
10
+ import Parser, {
11
+ AsyncAPIDocumentInterface,
12
+ fromFile,
13
+ MessageInterface,
14
+ } from '@asyncapi/parser';
11
15
  import endent from 'endent';
12
16
  import * as fs from 'fs';
13
17
  import * as path from 'path';
14
18
  import { MessageCodegenOptions } from './message-codegen-options';
19
+ import {
20
+ ADDITIONAL_PROPERTIES_PRESET,
21
+ ANY_TO_UNKNOWN_PRESET,
22
+ EXPORT_TYPES_PRESET,
23
+ IMPORTS_PRESET,
24
+ NULLABLE_PROPERTY_TO_UNION_PRESET,
25
+ } from './presets';
26
+ import {
27
+ getChannelAction,
28
+ getMessageTitle,
29
+ getModelPathPrefix,
30
+ getPayloadTitle,
31
+ getRelativeDir,
32
+ getServiceId,
33
+ getServiceTitle,
34
+ removeXParserProperties,
35
+ toPosixPath,
36
+ } from './utils';
15
37
 
16
38
  /**
17
39
  * Processes AsyncAPI document and generates Typescript classes from it.
@@ -152,30 +174,38 @@ export class Codegen {
152
174
  * @param asyncApiFile - path to AsyncAPI document
153
175
  */
154
176
  async processAsyncAPIDocument(asyncApiFile: string): Promise<void> {
155
- const schemaContent = fs.readFileSync(asyncApiFile, 'utf8');
156
-
157
- const asyncApiDocument = await parse(schemaContent, { path: asyncApiFile });
177
+ const parser = new Parser();
178
+ const { document, diagnostics } = await fromFile(
179
+ parser,
180
+ asyncApiFile,
181
+ ).parse();
182
+
183
+ if (document === undefined) {
184
+ console.error(`Failed to parse ${asyncApiFile}.`);
185
+ console.error(diagnostics);
186
+ process.exit(1);
187
+ }
158
188
 
159
189
  //export Typescript models
160
190
  const tsModelsOutputPath = path.join(
161
191
  this.typesOutputRoot,
162
192
  getRelativeDir(this.schemaRoot, asyncApiFile),
163
193
  );
164
- await this.exportTsModels(asyncApiDocument, tsModelsOutputPath);
194
+ await this.exportTsModels(document, tsModelsOutputPath);
165
195
 
166
196
  //export JSON Schemas
167
197
  const schemasOutputPath = path.join(
168
198
  this.schemasOutputRoot,
169
199
  getRelativeDir(this.schemaRoot, asyncApiFile),
170
200
  );
171
- await this.exportSchemas(asyncApiDocument, schemasOutputPath);
201
+ await this.exportSchemas(document, schemasOutputPath);
172
202
 
173
203
  //export message settings
174
204
  const messagingSettingsOutputPath = path.join(
175
205
  this.messagingSettingsOutputRoot,
176
206
  getRelativeDir(this.schemaRoot, asyncApiFile),
177
207
  );
178
- await this.exportSettings(asyncApiDocument, messagingSettingsOutputPath);
208
+ await this.exportSettings(document, messagingSettingsOutputPath);
179
209
  }
180
210
 
181
211
  /**
@@ -184,28 +214,37 @@ export class Codegen {
184
214
  * @param outputPath - output path for generated files
185
215
  */
186
216
  async exportTsModels(
187
- asyncAPIDocument: AsyncAPIDocument,
217
+ asyncAPIDocument: AsyncAPIDocumentInterface,
188
218
  outputPath: string,
189
219
  ): Promise<void> {
190
220
  const generator = new TypeScriptGenerator({
191
221
  modelType: 'interface',
192
- namingConvention: CustomNamingConvention,
193
222
  enumType: 'union',
223
+ mapType: 'indexedObject',
224
+ constraints: {
225
+ propertyKey: typeScriptDefaultPropertyKeyConstraints({
226
+ NAMING_FORMATTER: (name) => FormatHelpers.toSnakeCase(name),
227
+ NO_RESERVED_KEYWORDS: (name) => {
228
+ // TODO: This is a hack to avoid reserved keywords in property names.
229
+ // Currently many non-reserved keywords are also being replaced: https://github.com/asyncapi/modelina/issues/1053
230
+ return name;
231
+ },
232
+ }),
233
+ },
234
+ // Order of presets matters!
194
235
  presets: [
195
- ADDITIONAL_PROPERTIES_PRESET,
196
236
  NULLABLE_PROPERTY_TO_UNION_PRESET,
197
- EXPORT_UNION_TYPE_PRESET,
198
- EXPORT_INTERFACE_PRESET,
199
- DESCRIPTION_PRESET,
237
+ ANY_TO_UNKNOWN_PRESET,
238
+ ADDITIONAL_PROPERTIES_PRESET,
239
+ EXPORT_TYPES_PRESET,
240
+ TS_DESCRIPTION_PRESET,
200
241
  IMPORTS_PRESET,
201
242
  ],
202
243
  });
203
244
 
204
- let outputModels = await await generator.generate(asyncAPIDocument as any);
205
- for (const key in asyncAPIDocument.components().schemas()) {
206
- const referencedModels = await generator.generate(
207
- asyncAPIDocument.components().schemas()[key].json(),
208
- );
245
+ let outputModels = await generator.generate(asyncAPIDocument);
246
+ for (const schema of asyncAPIDocument.components().schemas()) {
247
+ const referencedModels = await generator.generate(schema.json());
209
248
  outputModels = outputModels.map(
210
249
  (om) =>
211
250
  referencedModels.find((rm) => om.modelName === rm.modelName) || om,
@@ -256,12 +295,12 @@ export class Codegen {
256
295
  * @param outputPath - output path for generated files
257
296
  */
258
297
  async exportSchemas(
259
- asyncAPIDocument: AsyncAPIDocument,
298
+ asyncAPIDocument: AsyncAPIDocumentInterface,
260
299
  outputPath: string,
261
300
  ): Promise<void> {
262
- const groupedSchemas: { [key: string]: Message[] } = {};
301
+ const groupedSchemas: { [key: string]: MessageInterface[] } = {};
263
302
 
264
- for (const [, message] of asyncAPIDocument.allMessages()) {
303
+ for (const message of asyncAPIDocument.messages()) {
265
304
  const schemaPathPrefix = getModelPathPrefix(getMessageTitle(message));
266
305
  if (groupedSchemas[schemaPathPrefix] === undefined) {
267
306
  groupedSchemas[schemaPathPrefix] = [];
@@ -280,12 +319,16 @@ export class Codegen {
280
319
  recursive: true,
281
320
  });
282
321
  for (const msg of messages) {
283
- const outputSchemaPath = this.buildSchemaOutPath(
284
- groupOutPutPath,
285
- getMessageTitle(msg),
286
- );
287
- await this.bundleSchema(msg.originalPayload(), outputSchemaPath);
288
- schemaFiles.push(outputSchemaPath);
322
+ const payload = msg.payload();
323
+ if (payload !== undefined && payload.json() !== undefined) {
324
+ const filteredPayload = removeXParserProperties(payload.json());
325
+ const outputSchemaPath = this.buildSchemaOutPath(
326
+ groupOutPutPath,
327
+ getMessageTitle(msg),
328
+ );
329
+ await this.bundleSchema(filteredPayload, outputSchemaPath);
330
+ schemaFiles.push(outputSchemaPath);
331
+ }
289
332
  }
290
333
 
291
334
  //generate barrel export for all model in AsyncAPI document
@@ -303,33 +346,30 @@ export class Codegen {
303
346
  * @param outputPath - output path for generated files
304
347
  */
305
348
  async exportSettings(
306
- asyncAPIDocument: AsyncAPIDocument,
349
+ asyncAPIDocument: AsyncAPIDocumentInterface,
307
350
  outputPath: string,
308
351
  ): Promise<void> {
309
352
  const serviceTitle = getServiceTitle(asyncAPIDocument);
310
353
  const serviceId = getServiceId(asyncAPIDocument);
311
354
  const channelsData: ChannelData[] = [];
312
355
 
313
- for (const routingKey of asyncAPIDocument.channelNames()) {
314
- const channel = asyncAPIDocument.channel(routingKey);
315
- if (channel.hasBinding('amqp')) {
316
- const queueName = channel.binding('amqp')['queue']['name'];
317
-
318
- const payloadName = channel.hasSubscribe()
319
- ? channel.subscribe().message().payload().title()
320
- : channel.hasPublish()
321
- ? channel.publish().message().payload().title()
322
- : 'undefined';
356
+ for (const channel of asyncAPIDocument.channels()) {
357
+ const queueName = channel.bindings().get('amqp')?.value().queue.name;
323
358
 
359
+ if (queueName !== undefined) {
360
+ const routingKey = channel.address();
361
+ const action = getChannelAction(channel);
362
+ const payloadName = getPayloadTitle(channel) ?? 'undefined';
324
363
  channelsData.push({
325
- routingKey: routingKey,
326
- queueName: queueName,
327
- payloadName: payloadName,
328
- acceptedAction: channel.hasSubscribe() ? 'subscribe' : `publish`,
364
+ routingKey,
365
+ queueName,
366
+ payloadName,
367
+ acceptedAction: action,
329
368
  isMultiTenant: routingKey.includes('*.*'),
330
369
  });
331
370
  }
332
371
  }
372
+
333
373
  await this.generateMessagingSettings(
334
374
  serviceId,
335
375
  serviceTitle,
@@ -685,207 +725,3 @@ interface ChannelData {
685
725
  /** Channel multi tenancy */
686
726
  isMultiTenant: boolean;
687
727
  }
688
-
689
- function getRelativeDir(from: string, to: string): string {
690
- return fs.statSync(to).isFile()
691
- ? path.dirname(path.relative(from, to))
692
- : path.relative(from, to);
693
- }
694
-
695
- function getMessageTitle(message: Message): string {
696
- return (
697
- message.payload().title() ??
698
- message.extension('x-parser-message-name') ??
699
- message.payload().extension('x-parser-schema-id')
700
- );
701
- }
702
-
703
- /**
704
- * Retrieve Service Title from AsyncAPI document.
705
- * As Title returned value set in info.title property of document.
706
- * @param asyncAPIDocument - AsyncAPI document.
707
- */
708
- function getServiceTitle(asyncAPIDocument: AsyncAPIDocument): string {
709
- return asyncAPIDocument.info().title();
710
- }
711
-
712
- /**
713
- * Retrieve Service Id from AsyncAPI document.
714
- * As Id returned value set in extension property `x-service-id`.
715
- * If extension property is not set - returned Service Title in lower-kebab-case.
716
- * @param asyncAPIDocument - AsyncAPI document.
717
- */
718
- function getServiceId(asyncAPIDocument: AsyncAPIDocument): string {
719
- return (
720
- (asyncAPIDocument.extension('x-service-id') as string) ??
721
- FormatHelpers.toParamCase(getServiceTitle(asyncAPIDocument))
722
- );
723
- }
724
-
725
- /**
726
- * Converts a path into a POSIX path by replacing current path separator with `/`.
727
- * @param p - Path to convert.
728
- */
729
- function toPosixPath(p: string): string {
730
- return path.posix.join(...p.split(path.sep));
731
- }
732
-
733
- /**
734
- * Generating the path prefix for model.
735
- * model name ends with "command" - prefix "commands"
736
- * model name ends with "event" - prefix "events"
737
- * default prefix "types"
738
- * @param modelName - name of the model, defined in json schema, or AsyncAPI document
739
- */
740
- function getModelPathPrefix(modelName: string): string {
741
- return modelName
742
- ? modelName.toLowerCase().endsWith('command')
743
- ? 'commands'
744
- : modelName.toLowerCase().endsWith('event')
745
- ? 'events'
746
- : 'types'
747
- : 'types';
748
- }
749
-
750
- /**
751
- * Adding 'export' for interfaces
752
- */
753
- const EXPORT_INTERFACE_PRESET = {
754
- interface: {
755
- self({ content }) {
756
- return `export ${content}`;
757
- },
758
- property({ content }) {
759
- return content;
760
- },
761
- },
762
- };
763
-
764
- /**
765
- * Adding 'export' for union enumeration types
766
- */
767
- const EXPORT_UNION_TYPE_PRESET = {
768
- type: {
769
- async self({ renderer }) {
770
- return `export ${await renderer.defaultSelf()}`;
771
- },
772
- },
773
- };
774
-
775
- /**
776
- * Preset for TypeScriptGenerator, adds descriptions to types and properties.
777
- */
778
- const DESCRIPTION_PRESET = {
779
- interface: {
780
- self({ renderer, content, model }) {
781
- const desc = model.getFromOriginalInput('description');
782
- if (desc) {
783
- const renderedDesc = renderer.renderComments(desc);
784
- return `${renderedDesc}\n${content}`;
785
- }
786
- return content;
787
- },
788
- property({ renderer, model, content, propertyName }) {
789
- if (model.getFromOriginalInput('properties') !== undefined) {
790
- const property = model.getFromOriginalInput('properties')[propertyName];
791
- if (property !== undefined) {
792
- const desc = property['description'];
793
- if (desc) {
794
- const renderedDesc = renderer.renderComments(desc);
795
- return `${renderedDesc}\n${content}`;
796
- }
797
- }
798
- }
799
- return content;
800
- },
801
- },
802
- };
803
-
804
- /**
805
- * Preset for TypeScriptGenerator, additionalProperties added to models as "[k: string]: unknown;"
806
- */
807
- const ADDITIONAL_PROPERTIES_PRESET = {
808
- interface: {
809
- self({ content }) {
810
- return content;
811
- },
812
- property({ renderer, propertyName, property, type }) {
813
- if (type === PropertyType.additionalProperty) {
814
- return property.originalInput === true
815
- ? `\n[k: string]: unknown;`
816
- : renderer
817
- .renderProperty(propertyName, property)
818
- .replace('additionalProperties?:', '[k: string]:');
819
- }
820
- return renderer.renderProperty(propertyName, property, type);
821
- },
822
- },
823
- };
824
-
825
- /**
826
- * Preset for TypeScriptGenerator, adds imports to resulting model.
827
- */
828
- const IMPORTS_PRESET = {
829
- interface: {
830
- self({ content, model }) {
831
- const interfaceName = model.getFromOriginalInput('title');
832
- const interfacePrefix = getModelPathPrefix(interfaceName);
833
- const dependencies = model.getNearestDependencies(model);
834
- const imports = dependencies.map((dep) => ({
835
- class: FormatHelpers.toPascalCase(dep),
836
- location:
837
- interfacePrefix !== getModelPathPrefix(dep)
838
- ? `../${getModelPathPrefix(dep)}/${FormatHelpers.toParamCase(dep)}`
839
- : `./${FormatHelpers.toParamCase(dep)}`,
840
- }));
841
- return endent`
842
- ${imports
843
- .sort((a, b) => a.location.localeCompare(b.location))
844
- .map((i) => `import { ${i.class} } from '${i.location}';`)
845
- .join('\n')}
846
- ${content}`;
847
- },
848
- property({ content }) {
849
- return content;
850
- },
851
- },
852
- };
853
-
854
- /**
855
- * Preset for TypeScriptGenerator, sets nullable properties as unions in resulted models(compatibility with zapatos generated models).
856
- */
857
- const NULLABLE_PROPERTY_TO_UNION_PRESET = {
858
- interface: {
859
- self({ content }) {
860
- return content;
861
- },
862
- property({ renderer, model, content, propertyName }) {
863
- if (model.getFromOriginalInput('properties') !== undefined) {
864
- const property = model.getFromOriginalInput('properties')[propertyName];
865
- if (property !== undefined) {
866
- if (Array.isArray(property['type'])) {
867
- const renderProp = renderer.renderProperty(propertyName, property);
868
- return renderProp.replace(
869
- 'object',
870
- content.split(':')[1].replace(';', ''),
871
- );
872
- }
873
- }
874
- }
875
-
876
- return content;
877
- },
878
- },
879
- };
880
-
881
- /**
882
- * Extension for @asyncapi/modelina, to make sure that the type name is in pascal.
883
- */
884
- const CustomNamingConvention: CommonNamingConvention = {
885
- type: (name) => {
886
- if (!name) {
887
- return '';
888
- }
889
- return FormatHelpers.toPascalCase(name);
890
- },
891
- };
@@ -0,0 +1,138 @@
1
+ import { FormatHelpers, TypeScriptPreset } from '@asyncapi/modelina';
2
+ import endent from 'endent';
3
+ import {
4
+ ensureNullable,
5
+ getModelPathPrefix,
6
+ indexOfNullArrayProperty,
7
+ indexOfNullObjectProperty,
8
+ replaceAnyType,
9
+ } from './utils';
10
+
11
+ /**
12
+ * Exports types.
13
+ */
14
+ export const EXPORT_TYPES_PRESET: TypeScriptPreset = {
15
+ interface: {
16
+ self({ content }) {
17
+ return `export ${content}`;
18
+ },
19
+ },
20
+ enum: {
21
+ self({ content }) {
22
+ return `export ${content}`;
23
+ },
24
+ },
25
+ type: {
26
+ self({ content }) {
27
+ return `export ${content}`;
28
+ },
29
+ },
30
+ };
31
+
32
+ /**
33
+ * Preset for TypeScriptGenerator, additionalProperties added to models as "[k: string]: unknown;"
34
+ */
35
+ export const ADDITIONAL_PROPERTIES_PRESET: TypeScriptPreset = {
36
+ interface: {
37
+ self({ content }) {
38
+ return content;
39
+ },
40
+ property({ property, content }) {
41
+ if (property.unconstrainedPropertyName === 'additionalProperties') {
42
+ return `\n[k: string]: unknown;`;
43
+ }
44
+ return content;
45
+ },
46
+ },
47
+ };
48
+
49
+ /**
50
+ * Preset for TypeScriptGenerator, adds imports to resulting model.
51
+ */
52
+ export const IMPORTS_PRESET: TypeScriptPreset = {
53
+ interface: {
54
+ self({ content, model }) {
55
+ const interfacePrefix = getModelPathPrefix(model.name);
56
+ const dependencies = model.getNearestDependencies();
57
+
58
+ const imports = dependencies.map((dep) => ({
59
+ class: FormatHelpers.toPascalCase(dep.name),
60
+ location:
61
+ interfacePrefix !== getModelPathPrefix(dep.name)
62
+ ? `../${getModelPathPrefix(dep.name)}/${FormatHelpers.toParamCase(
63
+ dep.name,
64
+ )}`
65
+ : `./${FormatHelpers.toParamCase(dep.name)}`,
66
+ }));
67
+ return endent`
68
+ ${imports
69
+ .sort((a, b) => a.location.localeCompare(b.location))
70
+ .map((i) => `import { ${i.class} } from '${i.location}';`)
71
+ .join('\n')}
72
+ ${content}`;
73
+ },
74
+ property({ content }) {
75
+ return content;
76
+ },
77
+ },
78
+ };
79
+
80
+ /**
81
+ * Preset for TypeScriptGenerator, sets nullable properties as unions in resulted models(compatibility with zapatos generated models).
82
+ */
83
+ export const NULLABLE_PROPERTY_TO_UNION_PRESET: TypeScriptPreset = {
84
+ interface: {
85
+ self({ content }) {
86
+ return content;
87
+ },
88
+ property({ renderer, property, content }) {
89
+ let index = indexOfNullObjectProperty(property);
90
+
91
+ // Regular object property, not array.
92
+ if (index > -1) {
93
+ property.property.type = ensureNullable(property.property.type, index);
94
+ return renderer.renderProperty(property);
95
+ }
96
+
97
+ // Array property.
98
+ index = indexOfNullArrayProperty(property);
99
+ if (index > -1) {
100
+ // This pattern is not perfect but it's good enough for this use case.
101
+ const regex = /((?<single>\w+)|\((?<multiple>[\w\s|]+)\))\[\]/;
102
+ const match = regex.exec(property.property.type);
103
+ if (match && (match?.groups?.single || match?.groups?.multiple)) {
104
+ const newType = ensureNullable(
105
+ match.groups.single ?? match.groups.multiple,
106
+ index,
107
+ );
108
+ // At this point we assume that the type will be a union of at least two types - null and something else.
109
+ property.property.type = `(${newType})[]`;
110
+ return renderer.renderProperty(property);
111
+ }
112
+ }
113
+
114
+ return content;
115
+ },
116
+ },
117
+ };
118
+
119
+ /**
120
+ * Converts all occurrences of 'any' to 'unknown'.
121
+ */
122
+ export const ANY_TO_UNKNOWN_PRESET: TypeScriptPreset = {
123
+ interface: {
124
+ property({ renderer, property }) {
125
+ property.property.type = replaceAnyType(
126
+ property.property.type,
127
+ 'unknown',
128
+ );
129
+ return renderer.renderProperty(property);
130
+ },
131
+ },
132
+ type: {
133
+ self({ content }) {
134
+ // TODO: This is inconsistent with interface but not sure how to use type renderer.
135
+ return replaceAnyType(content, 'unknown');
136
+ },
137
+ },
138
+ };