@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.
@@ -0,0 +1,258 @@
1
+ import {
2
+ ConstrainedArrayModel,
3
+ ConstrainedMetaModel,
4
+ ConstrainedObjectPropertyModel,
5
+ ConstrainedUnionModel,
6
+ FormatHelpers,
7
+ } from '@asyncapi/modelina';
8
+ import {
9
+ AsyncAPIDocumentInterface,
10
+ ChannelInterface,
11
+ MessageInterface,
12
+ SpecTypesV2,
13
+ } from '@asyncapi/parser';
14
+ import * as fs from 'fs';
15
+ import * as path from 'path';
16
+
17
+ export function getRelativeDir(from: string, to: string): string {
18
+ return fs.statSync(to).isFile()
19
+ ? path.dirname(path.relative(from, to))
20
+ : path.relative(from, to);
21
+ }
22
+
23
+ export function getMessageTitle(message: MessageInterface): string {
24
+ const messageTitle =
25
+ message.payload()?.title() ??
26
+ message.extensions().get('x-parser-message-name')?.value() ??
27
+ message.payload()?.extensions().get('x-parser-schema-id')?.value();
28
+
29
+ if (messageTitle === undefined) {
30
+ throw new Error('Unable to determine message title.');
31
+ }
32
+
33
+ return messageTitle;
34
+ }
35
+
36
+ /**
37
+ * Retrieve Service Title from AsyncAPI document.
38
+ * As Title returned value set in info.title property of document.
39
+ * @param asyncAPIDocument - AsyncAPI document.
40
+ */
41
+ export function getServiceTitle(
42
+ asyncAPIDocument: AsyncAPIDocumentInterface,
43
+ ): string {
44
+ return asyncAPIDocument.info().title();
45
+ }
46
+
47
+ /**
48
+ * Retrieve Service Id from AsyncAPI document.
49
+ * As Id returned value set in extension property `x-service-id`.
50
+ * If extension property is not set - returned Service Title in lower-kebab-case.
51
+ * @param asyncAPIDocument - AsyncAPI document.
52
+ */
53
+ export function getServiceId(
54
+ asyncAPIDocument: AsyncAPIDocumentInterface,
55
+ ): string {
56
+ const serviceId =
57
+ asyncAPIDocument.extensions().get('x-service-id')?.value() ??
58
+ asyncAPIDocument.info().title();
59
+
60
+ return FormatHelpers.toParamCase(serviceId);
61
+ }
62
+
63
+ /**
64
+ * Retrieves whether a channel is for subscribing or publishing.
65
+ * Currently, only channels with a single operation are supported.
66
+ * @param channel - Channel object.
67
+ */
68
+ export function getChannelAction(
69
+ channel: ChannelInterface,
70
+ ): 'subscribe' | 'publish' {
71
+ const operations = channel.operations();
72
+ if (operations.length === 0) {
73
+ throw new Error('Channel has no operations');
74
+ }
75
+ if (operations.length > 1) {
76
+ throw new Error('Channel has more than one operation');
77
+ }
78
+
79
+ const action = operations[0].action();
80
+
81
+ if (action !== 'subscribe' && action !== 'publish') {
82
+ throw new Error(`Channel has unsupported action: ${action}`);
83
+ }
84
+
85
+ return action;
86
+ }
87
+
88
+ /**
89
+ * Retrieves the payload model name for a channel.
90
+ * Currently, only channels with a single message are supported.
91
+ * @param channel - Channel object.
92
+ */
93
+ export function getPayloadTitle(channel: ChannelInterface): string | undefined {
94
+ const messages = channel.messages();
95
+ if (messages.length === 0) {
96
+ throw new Error('Channel has no messages');
97
+ }
98
+ if (messages.length > 1) {
99
+ throw new Error('Channel has more than one message');
100
+ }
101
+
102
+ return messages[0].payload()?.title();
103
+ }
104
+
105
+ /**
106
+ * Converts a path into a POSIX path by replacing current path separator with `/`.
107
+ * @param p - Path to convert.
108
+ */
109
+ export function toPosixPath(p: string): string {
110
+ return path.posix.join(...p.split(path.sep));
111
+ }
112
+
113
+ /**
114
+ * Generating the path prefix for model.
115
+ * model name ends with "command" - prefix "commands"
116
+ * model name ends with "event" - prefix "events"
117
+ * default prefix "types"
118
+ * @param modelName - name of the model, defined in json schema, or AsyncAPI document
119
+ */
120
+ export function getModelPathPrefix(modelName: string): string {
121
+ return modelName
122
+ ? modelName.toLowerCase().endsWith('command')
123
+ ? 'commands'
124
+ : modelName.toLowerCase().endsWith('event')
125
+ ? 'events'
126
+ : 'types'
127
+ : 'types';
128
+ }
129
+
130
+ export function removeXParserProperties(
131
+ obj: SpecTypesV2.AsyncAPISchemaObject,
132
+ ): SpecTypesV2.AsyncAPISchemaObject {
133
+ if (Array.isArray(obj)) {
134
+ return obj.map(removeXParserProperties);
135
+ } else if (obj !== null && typeof obj === 'object') {
136
+ const newObj = {};
137
+ for (const [key, value] of Object.entries(obj)) {
138
+ if (!key.startsWith('x-parser-')) {
139
+ newObj[key] = removeXParserProperties(value);
140
+ }
141
+ }
142
+ return newObj;
143
+ } else {
144
+ return obj;
145
+ }
146
+ }
147
+
148
+ /**
149
+ * Replaces the `any` type in a type string.
150
+ */
151
+ export function replaceAnyType(type: string, replacement: string): string {
152
+ const pattern = /\b(any)\b/g;
153
+ return type.replace(pattern, replacement);
154
+ }
155
+
156
+ export function indexOfNullPlain(
157
+ model: ConstrainedObjectPropertyModel,
158
+ ): number {
159
+ if (
160
+ model.property instanceof ConstrainedMetaModel &&
161
+ model.property.originalInput?.nullable === true
162
+ ) {
163
+ return 1; // Append null to the end of the type.
164
+ }
165
+ return -1;
166
+ }
167
+
168
+ export function indexOfNullUnion(
169
+ model: ConstrainedObjectPropertyModel,
170
+ ): number {
171
+ if (model.property instanceof ConstrainedUnionModel) {
172
+ return model.property.originalInput?.type?.indexOf('null') ?? -1;
173
+ }
174
+ return -1;
175
+ }
176
+
177
+ export function indexOfNullOneOf(
178
+ model: ConstrainedObjectPropertyModel,
179
+ ): number {
180
+ if (model.property instanceof ConstrainedUnionModel) {
181
+ const types = model.property.originalInput?.oneOf?.map((m) => m.type);
182
+ return types?.indexOf('null') ?? -1;
183
+ }
184
+ return -1;
185
+ }
186
+
187
+ export function indexOfNullArrayPlain(
188
+ model: ConstrainedObjectPropertyModel,
189
+ ): number {
190
+ if (
191
+ model.property instanceof ConstrainedArrayModel &&
192
+ model.property.originalInput?.items?.nullable === true
193
+ ) {
194
+ return 1; // Append null to the end of the type.
195
+ }
196
+ return -1;
197
+ }
198
+
199
+ export function indexOfNullArrayUnion(
200
+ model: ConstrainedObjectPropertyModel,
201
+ ): number {
202
+ if (model.property instanceof ConstrainedArrayModel) {
203
+ return model.property.originalInput?.items?.type?.indexOf('null') ?? -1;
204
+ }
205
+ return -1;
206
+ }
207
+
208
+ export function indexOfNullArrayOneOf(
209
+ model: ConstrainedObjectPropertyModel,
210
+ ): number {
211
+ if (model.property instanceof ConstrainedArrayModel) {
212
+ const types = model.property.originalInput?.items?.oneOf?.map(
213
+ (m) => m.type,
214
+ );
215
+ return types?.indexOf('null') ?? -1;
216
+ }
217
+ return -1;
218
+ }
219
+
220
+ export function indexOfNullObjectProperty(
221
+ model: ConstrainedObjectPropertyModel,
222
+ ): number {
223
+ if (indexOfNullPlain(model) > -1) {
224
+ return indexOfNullPlain(model);
225
+ }
226
+ if (indexOfNullUnion(model) > -1) {
227
+ return indexOfNullUnion(model);
228
+ }
229
+ if (indexOfNullOneOf(model) > -1) {
230
+ return indexOfNullOneOf(model);
231
+ }
232
+ return -1;
233
+ }
234
+
235
+ export function indexOfNullArrayProperty(
236
+ model: ConstrainedObjectPropertyModel,
237
+ ): number {
238
+ if (indexOfNullArrayPlain(model) > -1) {
239
+ return indexOfNullArrayPlain(model);
240
+ }
241
+ if (indexOfNullArrayUnion(model) > -1) {
242
+ return indexOfNullArrayUnion(model);
243
+ }
244
+ if (indexOfNullArrayOneOf(model) > -1) {
245
+ return indexOfNullArrayOneOf(model);
246
+ }
247
+ return -1;
248
+ }
249
+
250
+ export function ensureNullable(type: string, index: number): string {
251
+ const types = type.split('|').map((t) => t.trim());
252
+ if (types[index] === 'any') {
253
+ types[index] = replaceAnyType(types[index], 'null');
254
+ } else {
255
+ types.splice(index, 0, 'null');
256
+ }
257
+ return types.join(' | ');
258
+ }
@@ -1,4 +1,9 @@
1
- import { AsyncAPIDocument, parse } from '@asyncapi/parser';
1
+ import {
2
+ AsyncAPIDocumentInterface,
3
+ Diagnostic,
4
+ fromFile,
5
+ Parser,
6
+ } from '@asyncapi/parser';
2
7
  import * as fs from 'fs';
3
8
  import 'jest-extended';
4
9
  import { MessageDiffOptions } from './message-diff-options';
@@ -23,16 +28,28 @@ describe('sanity', () => {
23
28
  const loadDocument = async (
24
29
  file: string,
25
30
  editJson?: (any) => void,
26
- ): Promise<AsyncAPIDocument> => {
27
- const doc = await parse((await fs.promises.readFile(file)).toString(), {
28
- path: file,
29
- });
31
+ ): Promise<AsyncAPIDocumentInterface> => {
32
+ let document: AsyncAPIDocumentInterface | undefined;
33
+ let diagnostics: Diagnostic[] | undefined;
34
+ const parser = new Parser();
35
+ ({ document, diagnostics } = await fromFile(parser, file).parse());
36
+ if (document === undefined) {
37
+ throw new Error(`Failed to parse ${file}: ${diagnostics}`);
38
+ }
39
+
30
40
  if (!editJson) {
31
- return doc;
41
+ return document;
32
42
  }
33
- const json = doc.json();
43
+ const json = document.json();
34
44
  editJson(json);
35
- return parse(json);
45
+
46
+ ({ document, diagnostics } = await parser.parse(JSON.stringify(json)));
47
+
48
+ if (document === undefined) {
49
+ throw new Error(`Failed to parse edited JSON: ${diagnostics}`);
50
+ }
51
+
52
+ return document;
36
53
  };
37
54
 
38
55
  beforeEach(async () => {
@@ -127,7 +144,7 @@ describe('sanity', () => {
127
144
  `${resources}/0/1-asyncapi.yml`,
128
145
  (json) => {
129
146
  // remove an event
130
- delete json.channels['evt-channel'].publish.message;
147
+ delete json.channels['evt-channel']?.publish?.message;
131
148
  },
132
149
  );
133
150
 
@@ -246,8 +263,8 @@ describe('sanity', () => {
246
263
 
247
264
  // Act
248
265
  const result = await msgDiff.payloadDiff(
249
- document1.channels()['evt-channel'],
250
- document2.channels()['evt-channel'],
266
+ document1.channels().get('evt-channel'),
267
+ document2.channels().get('evt-channel'),
251
268
  );
252
269
 
253
270
  // Assert
@@ -272,8 +289,8 @@ describe('sanity', () => {
272
289
 
273
290
  // Act
274
291
  const result = await msgDiff.payloadDiff(
275
- document1.channels()['evt-channel'],
276
- document2.channels()['evt-channel'],
292
+ document1.channels().get('evt-channel'),
293
+ document2.channels().get('evt-channel'),
277
294
  );
278
295
 
279
296
  // Assert
@@ -291,11 +308,11 @@ describe('sanity', () => {
291
308
  },
292
309
  );
293
310
 
311
+ const channel1 = document1.channels().get('evt-channel');
312
+ const channel2 = document2.channels().get('evt-channel');
313
+
294
314
  // Act
295
- const result = await msgDiff.payloadDiff(
296
- document1.channels()['evt-channel'],
297
- document2.channels()['evt-channel'],
298
- );
315
+ const result = await msgDiff.payloadDiff(channel1, channel2);
299
316
 
300
317
  // Assert
301
318
  expect(result).toBe('changed');
@@ -314,8 +331,8 @@ describe('sanity', () => {
314
331
 
315
332
  // Act
316
333
  const result = await msgDiff.payloadDiff(
317
- document1.channels()['evt-channel'],
318
- document2.channels()['evt-channel'],
334
+ document1.channels().get('evt-channel'),
335
+ document2.channels().get('evt-channel'),
319
336
  );
320
337
 
321
338
  // Assert
@@ -338,8 +355,8 @@ describe('sanity', () => {
338
355
 
339
356
  // Act
340
357
  const result = await msgDiff.payloadDiff(
341
- document1.channels()['evt-channel'],
342
- document2.channels()['evt-channel'],
358
+ document1.channels().get('evt-channel'),
359
+ document2.channels().get('evt-channel'),
343
360
  );
344
361
 
345
362
  // Assert
@@ -359,8 +376,8 @@ describe('sanity', () => {
359
376
 
360
377
  // Act
361
378
  const result = await msgDiff.payloadDiff(
362
- document1.channels()['evt-channel'],
363
- document2.channels()['evt-channel'],
379
+ document1.channels().get('evt-channel'),
380
+ document2.channels().get('evt-channel'),
364
381
  );
365
382
 
366
383
  // Assert
@@ -380,8 +397,8 @@ describe('sanity', () => {
380
397
 
381
398
  // Act
382
399
  const result = await msgDiff.payloadDiff(
383
- document1.channels()['evt-channel'],
384
- document2.channels()['evt-channel'],
400
+ document1.channels().get('evt-channel'),
401
+ document2.channels().get('evt-channel'),
385
402
  );
386
403
 
387
404
  // Assert
@@ -401,8 +418,8 @@ describe('sanity', () => {
401
418
 
402
419
  // Act
403
420
  const result = await msgDiff.payloadDiff(
404
- document1.channels()['evt-channel'],
405
- document2.channels()['evt-channel'],
421
+ document1.channels().get('evt-channel'),
422
+ document2.channels().get('evt-channel'),
406
423
  );
407
424
 
408
425
  // Assert
@@ -1,7 +1,12 @@
1
1
  /* eslint-disable @typescript-eslint/no-explicit-any */
2
2
  /* eslint-disable no-console */
3
3
  import { diff, DiffOutputItem } from '@asyncapi/diff';
4
- import { AsyncAPIDocument, Channel, parse } from '@asyncapi/parser';
4
+ import Parser, {
5
+ AsyncAPIDocumentInterface,
6
+ ChannelInterface,
7
+ fromFile,
8
+ SchemaInterface,
9
+ } from '@asyncapi/parser';
5
10
  import { green, red, white } from 'chalk';
6
11
  import * as jsonDiff from 'diff';
7
12
  import * as fs from 'fs';
@@ -177,13 +182,17 @@ export class MessageDiff {
177
182
  console.log(
178
183
  `Comparing asyncapi document '${file2}'. Changes from branch '${this.branch}':`,
179
184
  );
180
- let document1: AsyncAPIDocument;
181
- let document2: AsyncAPIDocument;
185
+ let document1: AsyncAPIDocumentInterface;
186
+ let document2: AsyncAPIDocumentInterface;
182
187
 
183
188
  // load asyncapi document from git
184
189
  try {
185
- const text = fs.readFileSync(file1, 'utf8');
186
- document1 = await parse(text, { path: file1 });
190
+ const parser = new Parser();
191
+ const { document, diagnostics } = await fromFile(parser, file1).parse();
192
+ if (!document) {
193
+ throw `Error parsing ${file1}: ${diagnostics}`;
194
+ }
195
+ document1 = document;
187
196
  } catch (e: any) {
188
197
  throw `Error reading ${file1}: ${e}`;
189
198
  }
@@ -199,8 +208,12 @@ export class MessageDiff {
199
208
  this.results.breaking++;
200
209
  return;
201
210
  }
202
- const text = fs.readFileSync(file2, 'utf8');
203
- document2 = await parse(text, { path: file2 });
211
+ const parser = new Parser();
212
+ const { document, diagnostics } = await fromFile(parser, file2).parse();
213
+ if (!document) {
214
+ throw `Error parsing ${file2}: ${diagnostics}`;
215
+ }
216
+ document2 = document;
204
217
  } catch (e: any) {
205
218
  throw `Error reading ${file2}: ${e}`;
206
219
  }
@@ -214,14 +227,16 @@ export class MessageDiff {
214
227
 
215
228
  // diff payloads for channels which exist in both documents
216
229
  // removed / renamed channels are already marked as breaking
217
- for (const [k, channel1] of Object.entries(channels1)) {
218
- if (!(k in channels2)) {
230
+ for (const channel1 of channels1) {
231
+ if (channels2.has(channel1.id()) === false) {
219
232
  continue;
220
233
  }
221
234
  console.log(
222
- `Comparing payload for channel ${k} in document ${file2}. Changes from branch ${this.branch}`,
235
+ `Comparing payload for channel ${channel1.id()} in document ${file2}. Changes from branch ${
236
+ this.branch
237
+ }`,
223
238
  );
224
- const channel2 = channels2[k];
239
+ const channel2 = channels2.get(channel1.id());
225
240
  const resultKey = await this.payloadDiff(channel1, channel2);
226
241
  this.results[resultKey]++;
227
242
  }
@@ -232,8 +247,8 @@ export class MessageDiff {
232
247
  * Note: non standard rules are defined in file: asyncapi-override.ts
233
248
  */
234
249
  public async documentDiff(
235
- document1: AsyncAPIDocument,
236
- document2: AsyncAPIDocument,
250
+ document1: AsyncAPIDocumentInterface,
251
+ document2: AsyncAPIDocumentInterface,
237
252
  ): Promise<keyof Results> {
238
253
  // generate diff
239
254
  // overrides are applied through options here:
@@ -291,27 +306,38 @@ export class MessageDiff {
291
306
  * If any other version is used (in local file or git) then the comparison result will be 'skipped'.
292
307
  */
293
308
  public async payloadDiff(
294
- channel1: Channel,
295
- channel2: Channel,
309
+ channel1: ChannelInterface | undefined,
310
+ channel2: ChannelInterface | undefined,
296
311
  ): Promise<keyof Results> {
297
- const getChannelPayload = (channel: Channel): any => {
298
- const json = channel.json();
299
- return (json.subscribe ?? json.publish)?.message?.payload;
312
+ const getChannelPayload = (
313
+ channel: ChannelInterface | undefined,
314
+ ): SchemaInterface | undefined => {
315
+ if (channel === undefined) {
316
+ return undefined;
317
+ }
318
+
319
+ const messages = channel.messages();
320
+ if (messages.length > 1) {
321
+ throw new Error(
322
+ `Channel ${channel.id()} has more than one message. This is currently not supported.`,
323
+ );
324
+ }
325
+ return messages[0].payload();
300
326
  };
301
327
  const schema1 = getChannelPayload(channel1);
302
328
  const schema2 = getChannelPayload(channel2);
303
329
  if (schema1 === undefined) {
304
- throw `Could not find payload schema in ${this.branch}`;
330
+ throw new Error(`Could not find payload schema in ${this.branch}`);
305
331
  }
306
332
  if (schema2 === undefined) {
307
- throw `Could not find payload schema`;
333
+ throw new Error(`Could not find payload schema`);
308
334
  }
309
335
 
310
336
  let result: jsonSchemaDiff.DiffResult;
311
337
  try {
312
338
  result = await jsonSchemaDiff.diffSchemas({
313
- sourceSchema: schema1,
314
- destinationSchema: schema2,
339
+ sourceSchema: schema1.json(),
340
+ destinationSchema: schema2.json(),
315
341
  });
316
342
  } catch (e: any) {
317
343
  // Skip if unsupported schema version is used by either payload
@@ -1,4 +1,4 @@
1
- asyncapi: 2.0.0
1
+ asyncapi: 2.2.0
2
2
  info:
3
3
  title: Service 1
4
4
  version: '1.0.0'
@@ -8,7 +8,7 @@ info:
8
8
  x-service-id: ax-service-template
9
9
 
10
10
  channels:
11
- # commands
11
+ # commands
12
12
  'cmd-channel':
13
13
  bindings:
14
14
  amqp:
@@ -1,4 +1,4 @@
1
- asyncapi: 2.0.0
1
+ asyncapi: 2.2.0
2
2
  info:
3
3
  title: Service 2
4
4
  version: '1.0.0'
@@ -6,7 +6,7 @@ info:
6
6
  Testing 2
7
7
 
8
8
  channels:
9
- # commands
9
+ # commands
10
10
  'cmd-channel':
11
11
  bindings:
12
12
  amqp:
@@ -1,4 +1,4 @@
1
- asyncapi: 2.0.0
1
+ asyncapi: 2.2.0
2
2
  info:
3
3
  title: Template Service
4
4
  version: '1.0.0'