@ioka-technologies/asyncapi-rust-client-template 0.0.20

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,501 @@
1
+ /**
2
+ * Shared helper functions for Rust AsyncAPI NATS client template generation
3
+ *
4
+ * This module consolidates common utility functions used across multiple template files
5
+ * to reduce code duplication and ensure consistency.
6
+ */
7
+
8
+ /**
9
+ * Converts a string to a valid Rust identifier
10
+ * Handles special characters, keywords, and ensures valid Rust naming conventions
11
+ *
12
+ * @param {string} str - Input string to convert
13
+ * @returns {string} Valid Rust identifier
14
+ */
15
+ export function toRustIdentifier(str) {
16
+ if (!str) return 'unknown';
17
+ let identifier = str
18
+ .replace(/[^a-zA-Z0-9_]/g, '_')
19
+ .replace(/^[0-9]/, '_$&')
20
+ .replace(/_+/g, '_')
21
+ .replace(/^_+|_+$/g, '');
22
+ if (/^[0-9]/.test(identifier)) {
23
+ identifier = 'item_' + identifier;
24
+ }
25
+ if (!identifier) {
26
+ identifier = 'unknown';
27
+ }
28
+ const rustKeywords = [
29
+ 'as', 'break', 'const', 'continue', 'crate', 'else', 'enum', 'extern',
30
+ 'false', 'fn', 'for', 'if', 'impl', 'in', 'let', 'loop', 'match',
31
+ 'mod', 'move', 'mut', 'pub', 'ref', 'return', 'self', 'Self',
32
+ 'static', 'struct', 'super', 'trait', 'true', 'type', 'unsafe',
33
+ 'use', 'where', 'while', 'async', 'await', 'dyn'
34
+ ];
35
+ if (rustKeywords.includes(identifier)) {
36
+ identifier = identifier + '_';
37
+ }
38
+ return identifier;
39
+ }
40
+
41
+ /**
42
+ * Converts a string to PascalCase Rust type name
43
+ * Handles camelCase, snake_case, and kebab-case inputs
44
+ *
45
+ * @param {string} str - Input string to convert
46
+ * @returns {string} PascalCase Rust type name
47
+ */
48
+ export function toRustTypeName(str) {
49
+ if (!str) return 'Unknown';
50
+
51
+ // Ensure str is a string
52
+ const strValue = String(str);
53
+ const identifier = toRustIdentifier(strValue);
54
+
55
+ // Handle camelCase and PascalCase inputs by splitting on capital letters too
56
+ const parts = identifier
57
+ .replace(/([a-z])([A-Z])/g, '$1_$2') // Insert underscore before capital letters
58
+ .split(/[_\s-]+/) // Split on underscores, spaces, and hyphens
59
+ .filter(part => part.length > 0);
60
+
61
+ return parts
62
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
63
+ .join('');
64
+ }
65
+
66
+ /**
67
+ * Converts a string to snake_case Rust field name
68
+ *
69
+ * @param {string} str - Input string to convert
70
+ * @returns {string} snake_case Rust field name
71
+ */
72
+ export function toRustFieldName(str) {
73
+ if (!str) return 'unknown';
74
+ const identifier = toRustIdentifier(str);
75
+ return identifier
76
+ .replace(/([A-Z])/g, '_$1')
77
+ .toLowerCase()
78
+ .replace(/^_/, '')
79
+ .replace(/_+/g, '_');
80
+ }
81
+
82
+ /**
83
+ * Converts a string to Rust enum variant name (PascalCase)
84
+ *
85
+ * @param {string} str - Input string to convert
86
+ * @returns {string} PascalCase enum variant name
87
+ */
88
+ export function toRustEnumVariant(str) {
89
+ if (!str) return 'Unknown';
90
+ return str
91
+ .split(/[-_\s]+/)
92
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
93
+ .join('');
94
+ }
95
+
96
+ /**
97
+ * Converts a string to Rust enum variant with serde rename for lowercase serialization
98
+ *
99
+ * @param {string} str - Input string to convert
100
+ * @returns {object} Object with rustName (PascalCase) and serializedName (lowercase)
101
+ */
102
+ export function toRustEnumVariantWithSerde(str) {
103
+ if (!str) return { rustName: 'Unknown', serializedName: 'unknown' };
104
+
105
+ const rustName = str
106
+ .split(/[-_\s]+/)
107
+ .map(part => part.charAt(0).toUpperCase() + part.slice(1).toLowerCase())
108
+ .join('');
109
+
110
+ const serializedName = str.toLowerCase();
111
+
112
+ return { rustName, serializedName };
113
+ }
114
+
115
+ /**
116
+ * Gets the message type name from a message object
117
+ *
118
+ * @param {object} message - AsyncAPI message object
119
+ * @returns {string|null} Message type name or null if not found
120
+ */
121
+ export function getMessageTypeName(message) {
122
+ if (!message) return null;
123
+
124
+ try {
125
+ // Try AsyncAPI 3.x format first - check _meta and _json properties
126
+ if (message._meta && message._meta.id) {
127
+ return message._meta.id;
128
+ }
129
+ if (message._json && message._json['x-parser-message-name']) {
130
+ return message._json['x-parser-message-name'];
131
+ }
132
+ if (message._json && message._json['x-parser-unique-object-id']) {
133
+ return message._json['x-parser-unique-object-id'];
134
+ }
135
+
136
+ // Try different ways to get the message name
137
+ if (message.name && typeof message.name === 'function') {
138
+ return message.name();
139
+ }
140
+ if (message.name && typeof message.name === 'string') {
141
+ return message.name;
142
+ }
143
+ if (message.title && typeof message.title === 'function') {
144
+ return message.title();
145
+ }
146
+ if (message.title && typeof message.title === 'string') {
147
+ return message.title;
148
+ }
149
+
150
+ // Try to extract from $ref
151
+ if (message.$ref) {
152
+ return message.$ref.split('/').pop();
153
+ }
154
+
155
+ return null;
156
+ } catch (e) {
157
+ return null;
158
+ }
159
+ }
160
+
161
+ /**
162
+ * Gets the proper Rust type name from a message
163
+ *
164
+ * @param {object} message - AsyncAPI message object
165
+ * @returns {string} Rust type name
166
+ */
167
+ export function getMessageRustTypeName(message) {
168
+ const messageName = getMessageTypeName(message);
169
+ return messageName ? toRustTypeName(messageName) : 'UnknownMessage';
170
+ }
171
+
172
+ /**
173
+ * Gets the payload component schema Rust type name from a message
174
+ * This extracts the actual payload type (component schema) rather than the message wrapper
175
+ *
176
+ * @param {object} message - AsyncAPI message object
177
+ * @returns {string} Rust type name for the payload component schema
178
+ */
179
+ export function getPayloadRustTypeName(message) {
180
+ if (!message) return 'UnknownPayload';
181
+
182
+ try {
183
+ // First priority: For inline message schemas, get the message name itself
184
+ // This handles cases where the message is defined inline in channels
185
+ const messageName = getMessageTypeName(message);
186
+ if (messageName) {
187
+ // Check if this is a component message reference (has payload.$ref)
188
+ let payload = null;
189
+ if (message.payload && typeof message.payload === 'function') {
190
+ payload = message.payload();
191
+ } else if (message.payload) {
192
+ payload = message.payload;
193
+ }
194
+
195
+ // Check the message's _json for payload information
196
+ const messageJson = message._json || message;
197
+ if (!payload && messageJson.payload) {
198
+ payload = messageJson.payload;
199
+ }
200
+
201
+ // If payload has a $ref, this is a component message - extract the schema name
202
+ if (payload && payload.$ref) {
203
+ const refParts = payload.$ref.split('/');
204
+ const schemaName = refParts[refParts.length - 1];
205
+ return toRustTypeName(schemaName);
206
+ }
207
+
208
+ // For inline message schemas, use the message name directly as the payload type
209
+ // This is the correct approach for messages defined inline in channels
210
+ return toRustTypeName(messageName);
211
+ }
212
+
213
+ // Second priority: Try to get the payload schema reference from the message
214
+ let payload = null;
215
+
216
+ // Try different ways to access the payload
217
+ if (message.payload && typeof message.payload === 'function') {
218
+ payload = message.payload();
219
+ } else if (message.payload) {
220
+ payload = message.payload;
221
+ }
222
+
223
+ if (payload) {
224
+ // Check for $ref in the payload (direct reference to component schema)
225
+ if (payload.$ref) {
226
+ const refParts = payload.$ref.split('/');
227
+ const schemaName = refParts[refParts.length - 1];
228
+ return toRustTypeName(schemaName);
229
+ }
230
+
231
+ // Check for resolved $ref using x-parser-schema-id
232
+ if (payload['x-parser-schema-id']) {
233
+ return toRustTypeName(payload['x-parser-schema-id']);
234
+ }
235
+
236
+ // Check for x-parser-schema-id in _json
237
+ if (payload._json && payload._json['x-parser-schema-id']) {
238
+ return toRustTypeName(payload._json['x-parser-schema-id']);
239
+ }
240
+
241
+ // Check for title or name in the payload schema
242
+ if (payload.title) {
243
+ const title = typeof payload.title === 'function' ? payload.title() : payload.title;
244
+ if (title) return toRustTypeName(title);
245
+ }
246
+ if (payload.name) {
247
+ const name = typeof payload.name === 'function' ? payload.name() : payload.name;
248
+ if (name) return toRustTypeName(name);
249
+ }
250
+ }
251
+
252
+ // Check the message's _json for payload information
253
+ const messageJson = message._json || message;
254
+ if (messageJson.payload) {
255
+ if (messageJson.payload.$ref) {
256
+ const refParts = messageJson.payload.$ref.split('/');
257
+ const schemaName = refParts[refParts.length - 1];
258
+ return toRustTypeName(schemaName);
259
+ }
260
+ if (messageJson.payload['x-parser-schema-id']) {
261
+ return toRustTypeName(messageJson.payload['x-parser-schema-id']);
262
+ }
263
+ if (messageJson.payload.title) {
264
+ return toRustTypeName(messageJson.payload.title);
265
+ }
266
+ }
267
+
268
+ // Final fallback: try to extract from message title or name directly
269
+ if (message.title && typeof message.title === 'function') {
270
+ const title = message.title();
271
+ if (title && typeof title === 'string') {
272
+ return toRustTypeName(title);
273
+ }
274
+ } else if (message.title && typeof message.title === 'string') {
275
+ return toRustTypeName(message.title);
276
+ }
277
+
278
+ if (message.name && typeof message.name === 'function') {
279
+ const name = message.name();
280
+ if (name && typeof name === 'string') {
281
+ return toRustTypeName(name);
282
+ }
283
+ } else if (message.name && typeof message.name === 'string') {
284
+ return toRustTypeName(message.name);
285
+ }
286
+
287
+ // Check message._json for title/name
288
+ if (messageJson.title && typeof messageJson.title === 'string') {
289
+ return toRustTypeName(messageJson.title);
290
+ }
291
+ if (messageJson.name && typeof messageJson.name === 'string') {
292
+ return toRustTypeName(messageJson.name);
293
+ }
294
+
295
+ return 'UnknownPayload';
296
+ } catch (e) {
297
+ console.warn('Error extracting payload type name:', e.message);
298
+ return 'UnknownPayload';
299
+ }
300
+ }
301
+
302
+ /**
303
+ * Analyzes operations to determine client method patterns
304
+ * For NATS clients, we need to distinguish between:
305
+ * - Request/Reply operations (using NATS request/reply)
306
+ * - Publish operations (fire-and-forget)
307
+ * - Subscribe operations (message handlers)
308
+ *
309
+ * @param {Array} operations - Array of AsyncAPI operations
310
+ * @returns {Array} Array of client method patterns
311
+ */
312
+ export function analyzeClientOperations(operations) {
313
+ const patterns = [];
314
+
315
+ for (const operation of operations) {
316
+ const operationName = operation.id();
317
+ const action = operation.action();
318
+ const messages = operation.messages();
319
+
320
+ if (action === 'send') {
321
+ // Client sends messages - this becomes a client method
322
+ if (operation.reply && operation.reply()) {
323
+ // Request/Reply pattern
324
+ patterns.push({
325
+ type: 'request_reply',
326
+ operation,
327
+ operationName,
328
+ methodName: toRustFieldName(operationName),
329
+ requestMessage: messages[0],
330
+ responseMessage: operation.reply().messages()[0],
331
+ requestType: getPayloadRustTypeName(messages[0]),
332
+ responseType: getPayloadRustTypeName(operation.reply().messages()[0])
333
+ });
334
+ } else {
335
+ // Publish pattern (fire-and-forget)
336
+ patterns.push({
337
+ type: 'publish',
338
+ operation,
339
+ operationName,
340
+ methodName: toRustFieldName(operationName),
341
+ message: messages[0],
342
+ payloadType: getPayloadRustTypeName(messages[0])
343
+ });
344
+ }
345
+ } else if (action === 'receive') {
346
+ // Client receives messages - this becomes a subscription method
347
+ patterns.push({
348
+ type: 'subscribe',
349
+ operation,
350
+ operationName,
351
+ methodName: toRustFieldName(operationName.replace(/^receive/, 'subscribe_to_')),
352
+ message: messages[0],
353
+ payloadType: getPayloadRustTypeName(messages[0])
354
+ });
355
+ }
356
+ }
357
+
358
+ return patterns;
359
+ }
360
+
361
+ /**
362
+ * Gets the NATS subject from a channel address
363
+ *
364
+ * @param {object} channel - AsyncAPI channel object
365
+ * @returns {string} NATS subject
366
+ */
367
+ export function getNatsSubject(channel) {
368
+ try {
369
+ if (channel.address && typeof channel.address === 'function') {
370
+ return channel.address();
371
+ } else if (channel.address) {
372
+ return channel.address;
373
+ } else if (channel.id && typeof channel.id === 'function') {
374
+ return channel.id();
375
+ } else if (channel.id) {
376
+ return channel.id;
377
+ }
378
+ return 'unknown.subject';
379
+ } catch (e) {
380
+ return 'unknown.subject';
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Checks if a channel address contains variables (dynamic channel)
386
+ *
387
+ * @param {string} address - Channel address
388
+ * @returns {boolean} True if the address contains variables
389
+ */
390
+ export function isDynamicChannel(address) {
391
+ if (!address || typeof address !== 'string') return false;
392
+ return /\{[^}]+\}/.test(address);
393
+ }
394
+
395
+ /**
396
+ * Extracts variable names from a channel address
397
+ *
398
+ * @param {string} address - Channel address with variables
399
+ * @returns {Array<string>} Array of variable names
400
+ */
401
+ export function extractChannelVariables(address) {
402
+ if (!address || typeof address !== 'string') return [];
403
+ const matches = address.match(/\{([^}]+)\}/g);
404
+ if (!matches) return [];
405
+ return matches.map(match => match.slice(1, -1)); // Remove { and }
406
+ }
407
+
408
+ /**
409
+ * Gets channel parameters from a channel object
410
+ *
411
+ * @param {object} channel - AsyncAPI channel object
412
+ * @returns {Array<object>} Array of parameter objects with name and description
413
+ */
414
+ export function getChannelParameters(channel) {
415
+ try {
416
+ const parameters = [];
417
+
418
+ // Try to get parameters from the channel
419
+ let channelParams = null;
420
+ if (channel.parameters && typeof channel.parameters === 'function') {
421
+ channelParams = channel.parameters();
422
+ } else if (channel.parameters) {
423
+ channelParams = channel.parameters;
424
+ } else if (channel._json && channel._json.parameters) {
425
+ channelParams = channel._json.parameters;
426
+ }
427
+
428
+ if (channelParams) {
429
+ // Handle different parameter formats
430
+ if (typeof channelParams === 'object') {
431
+ for (const [paramName, paramDef] of Object.entries(channelParams)) {
432
+ // Skip internal AsyncAPI parser properties
433
+ if (paramName.startsWith('_') || paramName === 'collections' || paramName === 'meta') {
434
+ continue;
435
+ }
436
+
437
+ let description = 'Channel parameter';
438
+
439
+ if (paramDef && typeof paramDef === 'object') {
440
+ if (typeof paramDef.description === 'string') {
441
+ description = paramDef.description;
442
+ } else if (typeof paramDef.description === 'function') {
443
+ try {
444
+ description = paramDef.description();
445
+ } catch (e) {
446
+ description = 'Channel parameter';
447
+ }
448
+ } else if (paramDef._json && paramDef._json.description) {
449
+ description = paramDef._json.description;
450
+ }
451
+ } else if (typeof paramDef === 'string') {
452
+ description = paramDef;
453
+ }
454
+
455
+ parameters.push({
456
+ name: paramName,
457
+ description: description,
458
+ rustName: toRustFieldName(paramName),
459
+ rustType: 'String' // For now, assume all parameters are strings
460
+ });
461
+ }
462
+ }
463
+ }
464
+
465
+ return parameters;
466
+ } catch (e) {
467
+ console.warn('Error extracting channel parameters:', e.message);
468
+ return [];
469
+ }
470
+ }
471
+
472
+ /**
473
+ * Resolves a dynamic channel address with provided variable values
474
+ *
475
+ * @param {string} address - Channel address template with variables
476
+ * @param {object} variables - Object mapping variable names to values
477
+ * @returns {string} Resolved channel address
478
+ */
479
+ export function resolveChannelAddress(address, variables) {
480
+ if (!address || typeof address !== 'string') return address;
481
+ if (!variables || typeof variables !== 'object') return address;
482
+
483
+ let resolved = address;
484
+ for (const [varName, varValue] of Object.entries(variables)) {
485
+ const placeholder = `{${varName}}`;
486
+ resolved = resolved.replace(new RegExp(placeholder.replace(/[{}]/g, '\\$&'), 'g'), varValue);
487
+ }
488
+
489
+ return resolved;
490
+ }
491
+
492
+ /**
493
+ * Checks if a channel has dynamic parameters
494
+ *
495
+ * @param {object} channel - AsyncAPI channel object
496
+ * @returns {boolean} True if the channel has parameters
497
+ */
498
+ export function channelHasParameters(channel) {
499
+ const address = getNatsSubject(channel);
500
+ return isDynamicChannel(address);
501
+ }