@ioka-technologies/asyncapi-ts-client-template 0.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +382 -0
- package/USAGE.md +586 -0
- package/examples/simple/asyncapi.yaml +256 -0
- package/package.json +97 -0
- package/template/index.js +284 -0
- package/template/src/client.ts.js +377 -0
- package/template/src/index.ts.js +13 -0
- package/template/src/models.ts.js +229 -0
- package/template/src/runtime/errors.ts.js +29 -0
- package/template/src/runtime/transports/factory.ts.js +33 -0
- package/template/src/runtime/transports/http.ts.js +122 -0
- package/template/src/runtime/transports/websocket.ts.js +329 -0
- package/template/src/runtime/types.ts.js +75 -0
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
function generateClient(asyncapi, clientName) {
|
|
5
|
+
// Method name sanitization function
|
|
6
|
+
function sanitizeMethodName(operationId) {
|
|
7
|
+
if (!operationId) return 'unknownOperation';
|
|
8
|
+
|
|
9
|
+
// Convert to camelCase and remove invalid characters
|
|
10
|
+
let sanitized = operationId
|
|
11
|
+
// Replace dots, hyphens, underscores, spaces, and forward slashes with camelCase
|
|
12
|
+
.replace(/[.\-_\s\/]+(.)/g, (_, char) => char.toUpperCase())
|
|
13
|
+
// Remove any remaining invalid characters
|
|
14
|
+
.replace(/[^a-zA-Z0-9]/g, '')
|
|
15
|
+
// Ensure it starts with a lowercase letter if it starts with a number
|
|
16
|
+
.replace(/^[^a-zA-Z]/, 'operation')
|
|
17
|
+
// Ensure first character is lowercase
|
|
18
|
+
.replace(/^[A-Z]/, char => char.toLowerCase());
|
|
19
|
+
|
|
20
|
+
// Handle JavaScript reserved words
|
|
21
|
+
const JS_RESERVED_WORDS = [
|
|
22
|
+
'break', 'case', 'catch', 'class', 'const', 'continue', 'debugger', 'default',
|
|
23
|
+
'delete', 'do', 'else', 'export', 'extends', 'finally', 'for', 'function',
|
|
24
|
+
'if', 'import', 'in', 'instanceof', 'new', 'return', 'super', 'switch',
|
|
25
|
+
'this', 'throw', 'try', 'typeof', 'var', 'void', 'while', 'with', 'yield',
|
|
26
|
+
'let', 'static', 'enum', 'implements', 'package', 'protected', 'interface',
|
|
27
|
+
'private', 'public', 'async', 'await'
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
if (JS_RESERVED_WORDS.includes(sanitized)) {
|
|
31
|
+
sanitized = 'operation' + sanitized.charAt(0).toUpperCase() + sanitized.slice(1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Handle conflicts with existing class methods
|
|
35
|
+
const CLASS_METHOD_NAMES = ['connect', 'disconnect', 'unsubscribe', 'constructor'];
|
|
36
|
+
if (CLASS_METHOD_NAMES.includes(sanitized)) {
|
|
37
|
+
sanitized = sanitized + 'Operation';
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return sanitized;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let content = `import { TransportFactory } from './runtime/transports/factory';
|
|
44
|
+
import { Transport, TransportConfig, RequestOptions, MessageEnvelope } from './runtime/types';
|
|
45
|
+
import * as Models from './models';
|
|
46
|
+
|
|
47
|
+
export class ${clientName} {
|
|
48
|
+
private transport: Transport;
|
|
49
|
+
private config: TransportConfig;
|
|
50
|
+
|
|
51
|
+
constructor(config: TransportConfig) {
|
|
52
|
+
this.config = config;
|
|
53
|
+
this.transport = TransportFactory.create(config);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async connect(): Promise<void> {
|
|
57
|
+
await this.transport.connect();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async disconnect(): Promise<void> {
|
|
61
|
+
await this.transport.disconnect();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Unsubscribe from a specific channel
|
|
66
|
+
* @param channel Channel to unsubscribe from
|
|
67
|
+
* @param callback Optional specific callback to remove
|
|
68
|
+
*/
|
|
69
|
+
unsubscribe(channel: string, callback?: (payload: any) => void): void {
|
|
70
|
+
this.transport.unsubscribe(channel, callback);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Generated operation methods
|
|
74
|
+
`;
|
|
75
|
+
|
|
76
|
+
// Generate methods for each operation (AsyncAPI v3.0.0)
|
|
77
|
+
if (asyncapi.operations) {
|
|
78
|
+
const operations = asyncapi.operations();
|
|
79
|
+
if (operations) {
|
|
80
|
+
// Handle AsyncAPI parser collection - use .all() method to get array
|
|
81
|
+
const operationArray = operations.all ? operations.all() : Object.values(operations);
|
|
82
|
+
operationArray.forEach((operation) => {
|
|
83
|
+
// Get operation ID from the operation object
|
|
84
|
+
let operationId = null;
|
|
85
|
+
if (operation._meta && operation._meta.id) {
|
|
86
|
+
operationId = operation._meta.id;
|
|
87
|
+
} else if (operation.id && typeof operation.id === 'function') {
|
|
88
|
+
operationId = operation.id();
|
|
89
|
+
} else if (operation.id) {
|
|
90
|
+
operationId = operation.id;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!operationId) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
// Get the channel reference
|
|
98
|
+
let channelRef = null;
|
|
99
|
+
let channelAddress = null;
|
|
100
|
+
|
|
101
|
+
// Get channel information from embedded channel data (AsyncAPI v3.x approach)
|
|
102
|
+
const embeddedChannel = operation._json && operation._json.channel;
|
|
103
|
+
|
|
104
|
+
if (embeddedChannel) {
|
|
105
|
+
// Get the channel unique object ID
|
|
106
|
+
const embeddedChannelId = embeddedChannel['x-parser-unique-object-id'];
|
|
107
|
+
|
|
108
|
+
if (embeddedChannelId) {
|
|
109
|
+
// Find the channel by its ID
|
|
110
|
+
const channels = asyncapi.channels();
|
|
111
|
+
|
|
112
|
+
// Look for the channel with matching ID
|
|
113
|
+
for (const [channelKey, channel] of Object.entries(channels || {})) {
|
|
114
|
+
if (channel.id && channel.id() === embeddedChannelId) {
|
|
115
|
+
if (channel.address && typeof channel.address === 'function') {
|
|
116
|
+
channelAddress = channel.address();
|
|
117
|
+
} else if (channel.address) {
|
|
118
|
+
channelAddress = channel.address;
|
|
119
|
+
}
|
|
120
|
+
break;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Fallback: try to get channel from $ref (AsyncAPI v2.x approach)
|
|
127
|
+
if (!channelAddress && operation._json && operation._json.channel && operation._json.channel.$ref) {
|
|
128
|
+
channelRef = operation._json.channel.$ref;
|
|
129
|
+
|
|
130
|
+
const channelName = channelRef.split('/').pop();
|
|
131
|
+
|
|
132
|
+
const channels = asyncapi.channels();
|
|
133
|
+
if (channels && channels[channelName]) {
|
|
134
|
+
const channel = channels[channelName];
|
|
135
|
+
if (channel.address && typeof channel.address === 'function') {
|
|
136
|
+
channelAddress = channel.address();
|
|
137
|
+
} else if (channel.address) {
|
|
138
|
+
channelAddress = channel.address;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Get the action (send/receive)
|
|
144
|
+
let action = 'send';
|
|
145
|
+
if (operation.action && typeof operation.action === 'function') {
|
|
146
|
+
action = operation.action();
|
|
147
|
+
} else if (operation.action) {
|
|
148
|
+
action = operation.action;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Get message types for this operation
|
|
152
|
+
let messageTypes = [];
|
|
153
|
+
try {
|
|
154
|
+
if (operation.messages && typeof operation.messages === 'function') {
|
|
155
|
+
const messages = operation.messages();
|
|
156
|
+
if (messages && typeof messages.all === 'function') {
|
|
157
|
+
// Handle AsyncAPI parser collection
|
|
158
|
+
const messageArray = messages.all();
|
|
159
|
+
messageTypes = messageArray.map(msg => {
|
|
160
|
+
// Try to get message name from various sources
|
|
161
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
162
|
+
return msg.name();
|
|
163
|
+
} else if (msg.name) {
|
|
164
|
+
return msg.name;
|
|
165
|
+
} else if (msg._json && msg._json.name) {
|
|
166
|
+
return msg._json.name;
|
|
167
|
+
} else if (msg.$ref) {
|
|
168
|
+
return msg.$ref.split('/').pop();
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}).filter(Boolean);
|
|
172
|
+
} else if (Array.isArray(messages)) {
|
|
173
|
+
messageTypes = messages.map(msg => {
|
|
174
|
+
// Try to get message name from various sources
|
|
175
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
176
|
+
return msg.name();
|
|
177
|
+
} else if (msg.name) {
|
|
178
|
+
return msg.name;
|
|
179
|
+
} else if (msg._json && msg._json.name) {
|
|
180
|
+
return msg._json.name;
|
|
181
|
+
} else if (msg.$ref) {
|
|
182
|
+
return msg.$ref.split('/').pop();
|
|
183
|
+
}
|
|
184
|
+
return null;
|
|
185
|
+
}).filter(Boolean);
|
|
186
|
+
}
|
|
187
|
+
} else if (operation.messages && Array.isArray(operation.messages)) {
|
|
188
|
+
messageTypes = operation.messages.map(msg => {
|
|
189
|
+
// Try to get message name from various sources
|
|
190
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
191
|
+
return msg.name();
|
|
192
|
+
} else if (msg.name) {
|
|
193
|
+
return msg.name;
|
|
194
|
+
} else if (msg._json && msg._json.name) {
|
|
195
|
+
return msg._json.name;
|
|
196
|
+
} else if (msg.$ref) {
|
|
197
|
+
return msg.$ref.split('/').pop();
|
|
198
|
+
}
|
|
199
|
+
return null;
|
|
200
|
+
}).filter(Boolean);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Fallback: try to get from operation._json.messages
|
|
204
|
+
if (messageTypes.length === 0 && operation._json && operation._json.messages) {
|
|
205
|
+
messageTypes = operation._json.messages.map(msg => {
|
|
206
|
+
if (msg.name) {
|
|
207
|
+
return msg.name;
|
|
208
|
+
} else if (msg['x-parser-unique-object-id']) {
|
|
209
|
+
return msg['x-parser-unique-object-id'];
|
|
210
|
+
}
|
|
211
|
+
return null;
|
|
212
|
+
}).filter(Boolean);
|
|
213
|
+
}
|
|
214
|
+
} catch (msgError) {
|
|
215
|
+
// If we can't get messages, try to infer from channel
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (channelAddress) {
|
|
219
|
+
if (action === 'send') {
|
|
220
|
+
// Check if this is a request/response pattern
|
|
221
|
+
let hasReply = false;
|
|
222
|
+
let replyMessageTypes = [];
|
|
223
|
+
|
|
224
|
+
// Try multiple ways to access reply information
|
|
225
|
+
try {
|
|
226
|
+
// Method 1: Check operation.reply() function
|
|
227
|
+
if (operation.reply && typeof operation.reply === 'function') {
|
|
228
|
+
const reply = operation.reply();
|
|
229
|
+
if (reply) {
|
|
230
|
+
hasReply = true;
|
|
231
|
+
|
|
232
|
+
// Get reply messages
|
|
233
|
+
if (reply.messages && typeof reply.messages === 'function') {
|
|
234
|
+
const replyMessages = reply.messages();
|
|
235
|
+
if (replyMessages && typeof replyMessages.all === 'function') {
|
|
236
|
+
const messageArray = replyMessages.all();
|
|
237
|
+
replyMessageTypes = messageArray.map(msg => {
|
|
238
|
+
if (msg.name && typeof msg.name === 'function') {
|
|
239
|
+
return msg.name();
|
|
240
|
+
} else if (msg.name) {
|
|
241
|
+
return msg.name;
|
|
242
|
+
} else if (msg._json && msg._json.name) {
|
|
243
|
+
return msg._json.name;
|
|
244
|
+
} else if (msg.$ref) {
|
|
245
|
+
return msg.$ref.split('/').pop();
|
|
246
|
+
}
|
|
247
|
+
return null;
|
|
248
|
+
}).filter(Boolean);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Method 2: Check operation._json.reply
|
|
255
|
+
if (!hasReply && operation._json && operation._json.reply) {
|
|
256
|
+
hasReply = true;
|
|
257
|
+
const reply = operation._json.reply;
|
|
258
|
+
|
|
259
|
+
if (reply.messages) {
|
|
260
|
+
replyMessageTypes = reply.messages.map(msg => {
|
|
261
|
+
if (msg.$ref) {
|
|
262
|
+
return msg.$ref.split('/').pop();
|
|
263
|
+
}
|
|
264
|
+
return null;
|
|
265
|
+
}).filter(Boolean);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
} catch (replyError) {
|
|
270
|
+
// Could not get reply messages for operation
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Sanitize the method name
|
|
274
|
+
const methodName = sanitizeMethodName(operationId);
|
|
275
|
+
|
|
276
|
+
if (hasReply) {
|
|
277
|
+
// Request/Response pattern - send and wait for response
|
|
278
|
+
const requestPayloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
279
|
+
const responseType = replyMessageTypes.length > 0 ? `Models.${replyMessageTypes[0]}Payload` : 'any';
|
|
280
|
+
|
|
281
|
+
content += `
|
|
282
|
+
/**
|
|
283
|
+
* ${methodName} - Request/Response operation
|
|
284
|
+
* Original operation: ${operationId}
|
|
285
|
+
* Channel: ${channelAddress}
|
|
286
|
+
* @param payload Request payload
|
|
287
|
+
* @param options Request options
|
|
288
|
+
* @returns Promise that resolves with the response
|
|
289
|
+
*/
|
|
290
|
+
async ${methodName}(payload: ${requestPayloadType}, options?: RequestOptions): Promise<${responseType}> {
|
|
291
|
+
const envelope: MessageEnvelope = {
|
|
292
|
+
operation: '${operationId}',
|
|
293
|
+
payload,
|
|
294
|
+
channel: '${channelAddress}'
|
|
295
|
+
};
|
|
296
|
+
return this.transport.send('${channelAddress}', envelope, options);
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
} else {
|
|
300
|
+
// Regular send operation (fire and forget)
|
|
301
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
302
|
+
content += `
|
|
303
|
+
/**
|
|
304
|
+
* ${methodName} - Send operation (fire and forget)
|
|
305
|
+
* Original operation: ${operationId}
|
|
306
|
+
* Channel: ${channelAddress}
|
|
307
|
+
*/
|
|
308
|
+
async ${methodName}(payload: ${payloadType}, options?: RequestOptions): Promise<void> {
|
|
309
|
+
const envelope: MessageEnvelope = {
|
|
310
|
+
operation: '${operationId}',
|
|
311
|
+
payload,
|
|
312
|
+
channel: '${channelAddress}'
|
|
313
|
+
};
|
|
314
|
+
await this.transport.send('${channelAddress}', envelope, options);
|
|
315
|
+
}
|
|
316
|
+
`;
|
|
317
|
+
}
|
|
318
|
+
} else if (action === 'receive') {
|
|
319
|
+
// Sanitize the method name
|
|
320
|
+
const methodName = sanitizeMethodName(operationId);
|
|
321
|
+
|
|
322
|
+
// Generate receive method (event listener setup)
|
|
323
|
+
const payloadType = messageTypes.length > 0 ? `Models.${messageTypes[0]}Payload` : 'any';
|
|
324
|
+
content += `
|
|
325
|
+
/**
|
|
326
|
+
* ${methodName} - Receive operation
|
|
327
|
+
* Original operation: ${operationId}
|
|
328
|
+
* Channel: ${channelAddress}
|
|
329
|
+
* @param callback Function to call when a message is received
|
|
330
|
+
* @returns Unsubscribe function to stop listening for messages
|
|
331
|
+
*/
|
|
332
|
+
${methodName}(callback: (payload: ${payloadType}) => void): () => void {
|
|
333
|
+
return this.transport.subscribe('${channelAddress}', (envelope: MessageEnvelope) => {
|
|
334
|
+
// Filter by operation to ensure we only handle messages for this specific operation
|
|
335
|
+
if (envelope.operation === '${operationId}') {
|
|
336
|
+
callback(envelope.payload);
|
|
337
|
+
}
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
`;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
// Skip operations that can't be processed
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
content += `
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export default ${clientName};
|
|
354
|
+
`;
|
|
355
|
+
|
|
356
|
+
return content;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
module.exports = function ({ asyncapi, params }) {
|
|
360
|
+
const title = asyncapi.info().title();
|
|
361
|
+
|
|
362
|
+
// Always use the processed title, ignore params.clientName if it contains template variables
|
|
363
|
+
let clientName = `${title.replace(/[^a-zA-Z0-9]/g, '')}Client`;
|
|
364
|
+
|
|
365
|
+
// Only use params.clientName if it doesn't contain template variables
|
|
366
|
+
if (params.clientName && !params.clientName.includes('{{')) {
|
|
367
|
+
clientName = params.clientName;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const generatedContent = generateClient(asyncapi, clientName);
|
|
371
|
+
|
|
372
|
+
return (
|
|
373
|
+
<File name="client.ts">
|
|
374
|
+
{generatedContent}
|
|
375
|
+
</File>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
module.exports = function ({ asyncapi, params }) {
|
|
5
|
+
return (
|
|
6
|
+
<File name="index.ts">
|
|
7
|
+
{`export * from './client';
|
|
8
|
+
export * from './models';
|
|
9
|
+
export * from './runtime/types';
|
|
10
|
+
export * from './runtime/errors';`}
|
|
11
|
+
</File>
|
|
12
|
+
);
|
|
13
|
+
}
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
function generateModels(asyncapi) {
|
|
5
|
+
let content = `// Generated TypeScript models from AsyncAPI specification\n\n`;
|
|
6
|
+
|
|
7
|
+
// Helper functions for TypeScript identifier generation
|
|
8
|
+
function toTypeScriptIdentifier(str) {
|
|
9
|
+
if (!str) return 'unknown';
|
|
10
|
+
let identifier = str
|
|
11
|
+
.replace(/[^a-zA-Z0-9_]/g, '_')
|
|
12
|
+
.replace(/^[0-9]/, '_$&')
|
|
13
|
+
.replace(/_+/g, '_')
|
|
14
|
+
.replace(/^_+|_+$/g, '');
|
|
15
|
+
if (/^[0-9]/.test(identifier)) {
|
|
16
|
+
identifier = 'Item' + identifier;
|
|
17
|
+
}
|
|
18
|
+
if (!identifier) {
|
|
19
|
+
identifier = 'unknown';
|
|
20
|
+
}
|
|
21
|
+
return identifier;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function toTypeScriptTypeName(str) {
|
|
25
|
+
if (!str) return 'Unknown';
|
|
26
|
+
// Handle camelCase and PascalCase properly
|
|
27
|
+
const identifier = str
|
|
28
|
+
.replace(/[^a-zA-Z0-9]/g, '_')
|
|
29
|
+
.replace(/^[0-9]/, '_$&')
|
|
30
|
+
.replace(/_+/g, '_')
|
|
31
|
+
.replace(/^_+|_+$/g, '');
|
|
32
|
+
|
|
33
|
+
// Split on underscores and camelCase boundaries
|
|
34
|
+
const parts = identifier.split(/[_\s]+|(?=[A-Z])/);
|
|
35
|
+
|
|
36
|
+
return parts
|
|
37
|
+
.filter(part => part.length > 0)
|
|
38
|
+
.map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
|
|
39
|
+
.join('');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Extract message schemas and build channel mapping
|
|
43
|
+
const components = asyncapi.components();
|
|
44
|
+
const messageSchemas = [];
|
|
45
|
+
const messageToChannels = new Map();
|
|
46
|
+
const generatedTypes = new Set();
|
|
47
|
+
|
|
48
|
+
// First, build channel to message mapping
|
|
49
|
+
if (asyncapi.channels) {
|
|
50
|
+
const channels = asyncapi.channels();
|
|
51
|
+
if (channels) {
|
|
52
|
+
Object.entries(channels).forEach(([channelName, channel]) => {
|
|
53
|
+
try {
|
|
54
|
+
// Handle AsyncAPI 3.x format
|
|
55
|
+
if (channel.messages) {
|
|
56
|
+
const messages = channel.messages();
|
|
57
|
+
if (messages) {
|
|
58
|
+
Object.entries(messages).forEach(([msgKey, message]) => {
|
|
59
|
+
if (message) {
|
|
60
|
+
let messageName = null;
|
|
61
|
+
if (message.$ref) {
|
|
62
|
+
messageName = message.$ref.split('/').pop();
|
|
63
|
+
} else if (message.name) {
|
|
64
|
+
messageName = typeof message.name === 'function' ? message.name() : message.name;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (messageName) {
|
|
68
|
+
if (!messageToChannels.has(messageName)) {
|
|
69
|
+
messageToChannels.set(messageName, []);
|
|
70
|
+
}
|
|
71
|
+
messageToChannels.get(messageName).push(channelName);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Ignore channel processing errors
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Extract messages from components
|
|
85
|
+
if (components && components.messages) {
|
|
86
|
+
const messages = components.messages();
|
|
87
|
+
if (messages) {
|
|
88
|
+
Object.entries(messages).forEach(([name, message]) => {
|
|
89
|
+
// Skip internal AsyncAPI parser objects
|
|
90
|
+
if (name === 'collections' || name === '_meta' || name.startsWith('_')) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
let payload = null;
|
|
94
|
+
let description = null;
|
|
95
|
+
let title = null;
|
|
96
|
+
let messageName = name;
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (message.payload && typeof message.payload === 'function') {
|
|
100
|
+
const payloadSchema = message.payload();
|
|
101
|
+
payload = payloadSchema && payloadSchema.json ? payloadSchema.json() : payloadSchema;
|
|
102
|
+
}
|
|
103
|
+
description = message.description && typeof message.description === 'function' ? message.description() : message.description;
|
|
104
|
+
title = message.title && typeof message.title === 'function' ? message.title() : message.title;
|
|
105
|
+
|
|
106
|
+
// Try to get the actual message name
|
|
107
|
+
if (message.name && typeof message.name === 'function') {
|
|
108
|
+
messageName = message.name();
|
|
109
|
+
} else if (message.name) {
|
|
110
|
+
messageName = message.name;
|
|
111
|
+
}
|
|
112
|
+
} catch (e) {
|
|
113
|
+
// Ignore payload extraction errors
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
|
|
117
|
+
messageSchemas.push({
|
|
118
|
+
name: messageName,
|
|
119
|
+
typeName: toTypeScriptTypeName(messageName),
|
|
120
|
+
payload,
|
|
121
|
+
description: description || title,
|
|
122
|
+
channels
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Helper function to convert JSON schema to TypeScript type
|
|
129
|
+
function jsonSchemaToTypeScriptType(schema) {
|
|
130
|
+
if (!schema) return 'any';
|
|
131
|
+
|
|
132
|
+
// Handle $ref
|
|
133
|
+
if (schema.$ref) {
|
|
134
|
+
const refName = schema.$ref.split('/').pop();
|
|
135
|
+
return toTypeScriptTypeName(refName);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (!schema.type) {
|
|
139
|
+
// If no type specified, check for properties (object) or items (array)
|
|
140
|
+
if (schema.properties) {
|
|
141
|
+
schema.type = 'object';
|
|
142
|
+
} else if (schema.items) {
|
|
143
|
+
schema.type = 'array';
|
|
144
|
+
} else {
|
|
145
|
+
return 'any';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
switch (schema.type) {
|
|
150
|
+
case 'string':
|
|
151
|
+
if (schema.enum && schema.enum.length > 0) {
|
|
152
|
+
return schema.enum.map(val => `'${val}'`).join(' | ');
|
|
153
|
+
}
|
|
154
|
+
return 'string';
|
|
155
|
+
case 'integer':
|
|
156
|
+
case 'number':
|
|
157
|
+
return 'number';
|
|
158
|
+
case 'boolean':
|
|
159
|
+
return 'boolean';
|
|
160
|
+
case 'array': {
|
|
161
|
+
const itemType = jsonSchemaToTypeScriptType(schema.items);
|
|
162
|
+
return `${itemType}[]`;
|
|
163
|
+
}
|
|
164
|
+
case 'object':
|
|
165
|
+
return 'Record<string, any>';
|
|
166
|
+
default:
|
|
167
|
+
return 'any';
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Generate message interfaces
|
|
172
|
+
function generateMessageInterface(schema, messageName) {
|
|
173
|
+
if (!schema || !schema.properties) {
|
|
174
|
+
return ' [key: string]: any;';
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
|
|
178
|
+
const tsType = jsonSchemaToTypeScriptType(fieldSchema);
|
|
179
|
+
const optional = !schema.required || !schema.required.includes(fieldName);
|
|
180
|
+
const optionalMarker = optional ? '?' : '';
|
|
181
|
+
|
|
182
|
+
let fieldDoc = '';
|
|
183
|
+
if (fieldSchema.description) {
|
|
184
|
+
fieldDoc = ` /** ${fieldSchema.description} */\n`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `${fieldDoc} ${fieldName}${optionalMarker}: ${tsType};`;
|
|
188
|
+
}).join('\n');
|
|
189
|
+
|
|
190
|
+
return fields;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// Generate interfaces for each message
|
|
194
|
+
messageSchemas.forEach(schema => {
|
|
195
|
+
const doc = schema.description ? `/** ${schema.description} */\n` : `/** ${schema.name} message payload */\n`;
|
|
196
|
+
|
|
197
|
+
content += `${doc}export interface ${schema.typeName}Payload {\n`;
|
|
198
|
+
content += generateMessageInterface(schema.payload, schema.typeName);
|
|
199
|
+
content += `\n}\n\n`;
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
// Generate a union type for all message payloads
|
|
203
|
+
if (messageSchemas.length > 0) {
|
|
204
|
+
const payloadTypes = messageSchemas.map(schema => `${schema.typeName}Payload`).join(' | ');
|
|
205
|
+
content += `/** Union type for all message payloads */\n`;
|
|
206
|
+
content += `export type MessagePayload = ${payloadTypes};\n\n`;
|
|
207
|
+
|
|
208
|
+
// Generate message type constants
|
|
209
|
+
content += `/** Message type constants */\n`;
|
|
210
|
+
content += `export const MessageTypes = {\n`;
|
|
211
|
+
messageSchemas.forEach(schema => {
|
|
212
|
+
content += ` ${schema.typeName.toUpperCase()}: '${schema.name}',\n`;
|
|
213
|
+
});
|
|
214
|
+
content += `} as const;\n\n`;
|
|
215
|
+
|
|
216
|
+
content += `/** Message type union */\n`;
|
|
217
|
+
content += `export type MessageType = typeof MessageTypes[keyof typeof MessageTypes];\n\n`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return content;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = function ({ asyncapi, params }) {
|
|
224
|
+
return (
|
|
225
|
+
<File name="models.ts">
|
|
226
|
+
{generateModels(asyncapi)}
|
|
227
|
+
</File>
|
|
228
|
+
);
|
|
229
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
module.exports = function ({ asyncapi, params }) {
|
|
5
|
+
return (
|
|
6
|
+
<File name="errors.ts">
|
|
7
|
+
{`export class TransportError extends Error {
|
|
8
|
+
constructor(message: string) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = 'TransportError';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export class ConnectionError extends TransportError {
|
|
15
|
+
constructor(message: string) {
|
|
16
|
+
super(message);
|
|
17
|
+
this.name = 'ConnectionError';
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TimeoutError extends TransportError {
|
|
22
|
+
constructor(message: string) {
|
|
23
|
+
super(message);
|
|
24
|
+
this.name = 'TimeoutError';
|
|
25
|
+
}
|
|
26
|
+
}`}
|
|
27
|
+
</File>
|
|
28
|
+
);
|
|
29
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
module.exports = function ({ asyncapi, params }) {
|
|
5
|
+
const transports = (params.transports || 'websocket,http').split(',').map(t => t.trim());
|
|
6
|
+
|
|
7
|
+
let imports = `import { Transport, TransportConfig } from '../types';\n`;
|
|
8
|
+
let cases = '';
|
|
9
|
+
|
|
10
|
+
if (transports.includes('websocket')) {
|
|
11
|
+
imports += `import { WebSocketTransport } from './websocket';\n`;
|
|
12
|
+
cases += ` case 'websocket': return new WebSocketTransport(config);\n`;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
if (transports.includes('http')) {
|
|
16
|
+
imports += `import { HttpTransport } from './http';\n`;
|
|
17
|
+
cases += ` case 'http': return new HttpTransport(config);\n`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<File name="factory.ts">
|
|
22
|
+
{`${imports}
|
|
23
|
+
export class TransportFactory {
|
|
24
|
+
static create(config: TransportConfig): Transport {
|
|
25
|
+
switch (config.type) {
|
|
26
|
+
${cases} default:
|
|
27
|
+
throw new Error(\`Unsupported transport type: \${config.type}\`);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}`}
|
|
31
|
+
</File>
|
|
32
|
+
);
|
|
33
|
+
}
|