@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,625 @@
1
+ /* eslint-disable no-unused-vars */
2
+ import { File } from '@asyncapi/generator-react-sdk';
3
+ import {
4
+ toRustIdentifier,
5
+ toRustTypeName,
6
+ toRustFieldName,
7
+ toRustEnumVariant,
8
+ toRustEnumVariantWithSerde
9
+ } from '../helpers/index.js';
10
+
11
+ export default function ModelsRs({ asyncapi }) {
12
+
13
+ // Extract message schemas and build channel mapping
14
+ const components = asyncapi.components();
15
+ const messageSchemas = [];
16
+ const componentSchemas = [];
17
+ const messageToChannels = new Map();
18
+ const generatedTypes = new Set();
19
+ const nestedSchemas = new Map();
20
+ const schemaRegistry = new Map();
21
+
22
+ // First, build channel to message mapping and extract inline message schemas
23
+ if (asyncapi.channels) {
24
+ const channels = asyncapi.channels();
25
+ if (channels) {
26
+ // Use proper iteration for AsyncAPI collection
27
+ for (const channel of channels) {
28
+ try {
29
+ const channelName = channel.id();
30
+
31
+ // Handle AsyncAPI 3.x format - extract inline messages from channels
32
+ if (channel.messages) {
33
+ const messages = channel.messages();
34
+ if (messages) {
35
+ // Check if messages is an object with message names as keys
36
+ if (typeof messages === 'object' && !Array.isArray(messages)) {
37
+ // Iterate through message entries (messageName -> messageObject)
38
+ Object.entries(messages).forEach(([messageName, message]) => {
39
+ if (message && messageName) {
40
+ let payload = null;
41
+ let description = null;
42
+
43
+ // Get payload schema
44
+ if (message.payload && typeof message.payload === 'function') {
45
+ payload = message.payload();
46
+ if (payload && payload.json && typeof payload.json === 'function') {
47
+ payload = payload.json();
48
+ }
49
+ } else if (message.payload) {
50
+ payload = message.payload;
51
+ }
52
+
53
+ // Get description
54
+ if (message.description && typeof message.description === 'function') {
55
+ description = message.description();
56
+ } else if (message.description) {
57
+ description = message.description;
58
+ }
59
+
60
+ // Add to channel mapping
61
+ if (!messageToChannels.has(messageName)) {
62
+ messageToChannels.set(messageName, []);
63
+ }
64
+ messageToChannels.get(messageName).push(channelName);
65
+
66
+ // Add to message schemas for inline messages
67
+ messageSchemas.push({
68
+ name: messageName,
69
+ rustName: toRustTypeName(messageName),
70
+ payload,
71
+ rawPayload: payload,
72
+ description,
73
+ channels: [channelName]
74
+ });
75
+ }
76
+ });
77
+ } else {
78
+ // Try iterating as a collection
79
+ for (const message of messages) {
80
+ if (message) {
81
+ let messageName = null;
82
+ let payload = null;
83
+ let description = null;
84
+
85
+ // Get message name - try multiple approaches
86
+ if (message._meta && message._meta.id) {
87
+ messageName = message._meta.id;
88
+ } else if (message._json && message._json['x-parser-message-name']) {
89
+ messageName = message._json['x-parser-message-name'];
90
+ } else if (message._json && message._json['x-parser-unique-object-id']) {
91
+ messageName = message._json['x-parser-unique-object-id'];
92
+ } else if (message.name && typeof message.name === 'function') {
93
+ messageName = message.name();
94
+ } else if (message.name) {
95
+ messageName = message.name;
96
+ } else if (message.$ref) {
97
+ messageName = message.$ref.split('/').pop();
98
+ }
99
+
100
+ // Get payload schema
101
+ if (message.payload && typeof message.payload === 'function') {
102
+ payload = message.payload();
103
+ if (payload && payload.json && typeof payload.json === 'function') {
104
+ payload = payload.json();
105
+ }
106
+ } else if (message.payload) {
107
+ payload = message.payload;
108
+ }
109
+
110
+ // Get description
111
+ if (message.description && typeof message.description === 'function') {
112
+ description = message.description();
113
+ } else if (message.description) {
114
+ description = message.description;
115
+ }
116
+
117
+ if (messageName) {
118
+ // Add to channel mapping
119
+ if (!messageToChannels.has(messageName)) {
120
+ messageToChannels.set(messageName, []);
121
+ }
122
+ messageToChannels.get(messageName).push(channelName);
123
+
124
+ // Add to message schemas for inline messages
125
+ messageSchemas.push({
126
+ name: messageName,
127
+ rustName: toRustTypeName(messageName),
128
+ payload,
129
+ rawPayload: payload,
130
+ description,
131
+ channels: [channelName]
132
+ });
133
+ }
134
+ }
135
+ }
136
+ }
137
+ }
138
+ }
139
+ } catch (e) {
140
+ // Ignore channel processing errors
141
+ console.warn(`Error processing channel: ${e.message}`);
142
+ }
143
+ }
144
+ }
145
+ }
146
+
147
+ // Build schema registry from components.schemas
148
+ // Try to access the raw AsyncAPI document
149
+ let rawDoc = null;
150
+ try {
151
+ if (asyncapi.json && typeof asyncapi.json === 'function') {
152
+ rawDoc = asyncapi.json();
153
+ } else if (asyncapi._json) {
154
+ rawDoc = asyncapi._json;
155
+ }
156
+ } catch (e) {
157
+ // Ignore
158
+ }
159
+
160
+ // Extract schemas from raw document if available
161
+ if (rawDoc && rawDoc.components && rawDoc.components.schemas) {
162
+ Object.entries(rawDoc.components.schemas).forEach(([name, schema]) => {
163
+ if (name && typeof name === 'string' && schema && typeof schema === 'object') {
164
+ schemaRegistry.set(name, schema);
165
+ componentSchemas.push({
166
+ name,
167
+ rustName: toRustTypeName(name),
168
+ schema: schema,
169
+ description: schema.description
170
+ });
171
+ }
172
+ });
173
+ }
174
+
175
+ // Fallback: try the components.schemas() method
176
+ if (componentSchemas.length === 0 && components && components.schemas) {
177
+ try {
178
+ const schemas = components.schemas();
179
+ if (schemas) {
180
+ // Try different ways to access schemas
181
+ let schemaEntries = [];
182
+
183
+ if (schemas instanceof Map) {
184
+ schemaEntries = Array.from(schemas.entries());
185
+ } else if (typeof schemas === 'object') {
186
+ schemaEntries = Object.entries(schemas);
187
+ } else if (schemas.all && typeof schemas.all === 'function') {
188
+ // AsyncAPI parser might have an all() method
189
+ const allSchemas = schemas.all();
190
+ if (Array.isArray(allSchemas)) {
191
+ schemaEntries = allSchemas.map(schema => {
192
+ const name = schema.uid ? schema.uid() : (schema.id ? schema.id() : null);
193
+ return [name, schema];
194
+ }).filter(([name]) => name);
195
+ }
196
+ }
197
+
198
+ schemaEntries.forEach(([name, schema]) => {
199
+ // Skip internal AsyncAPI parser objects and numeric keys
200
+ if (!name || name === 'collections' || name === '_meta' || name.startsWith('_') || /^\d+$/.test(name)) {
201
+ return;
202
+ }
203
+
204
+ let schemaData = null;
205
+ let description = null;
206
+
207
+ try {
208
+ // Handle different schema object types
209
+ if (schema && typeof schema.json === 'function') {
210
+ schemaData = schema.json();
211
+ } else if (schema && typeof schema === 'object') {
212
+ schemaData = schema;
213
+ }
214
+
215
+ if (schema && typeof schema.description === 'function') {
216
+ description = schema.description();
217
+ } else if (schema && schema.description) {
218
+ description = schema.description;
219
+ }
220
+ } catch (e) {
221
+ // Ignore schema extraction errors
222
+ console.warn(`Failed to extract schema for ${name}:`, e.message);
223
+ }
224
+
225
+ if (schemaData && typeof name === 'string' && name.length > 0) {
226
+ schemaRegistry.set(name, schemaData);
227
+ componentSchemas.push({
228
+ name,
229
+ rustName: toRustTypeName(name),
230
+ schema: schemaData,
231
+ description
232
+ });
233
+ }
234
+ });
235
+ }
236
+ } catch (e) {
237
+ console.warn('Failed to extract component schemas:', e.message);
238
+ }
239
+ }
240
+
241
+ // Extract messages from components
242
+ if (components && components.messages) {
243
+ const messages = components.messages();
244
+ if (messages) {
245
+ Object.entries(messages).forEach(([name, message]) => {
246
+ let payload = null;
247
+ let description = null;
248
+ let title = null;
249
+ let messageName = name;
250
+
251
+ try {
252
+ let rawPayload = null;
253
+ if (message.payload && typeof message.payload === 'function') {
254
+ const payloadSchema = message.payload();
255
+ payload = payloadSchema && payloadSchema.json ? payloadSchema.json() : payloadSchema;
256
+ // Try to get the raw payload reference from the message
257
+ if (message._json && message._json.payload) {
258
+ rawPayload = message._json.payload;
259
+ }
260
+ }
261
+ description = message.description && typeof message.description === 'function' ? message.description() : null;
262
+ title = message.title && typeof message.title === 'function' ? message.title() : null;
263
+
264
+ // Try to get the actual message name
265
+ if (message.name && typeof message.name === 'function') {
266
+ messageName = message.name();
267
+ } else if (message.name) {
268
+ messageName = message.name;
269
+ }
270
+
271
+ // If we have raw document access, try to get the payload reference from there
272
+ if (!rawPayload && rawDoc && rawDoc.components && rawDoc.components.messages && rawDoc.components.messages[name]) {
273
+ rawPayload = rawDoc.components.messages[name].payload;
274
+ }
275
+
276
+ const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
277
+ messageSchemas.push({
278
+ name: messageName,
279
+ rustName: toRustTypeName(messageName),
280
+ payload,
281
+ rawPayload,
282
+ description: description || title,
283
+ channels
284
+ });
285
+ } catch (e) {
286
+ // Ignore payload extraction errors
287
+ const channels = messageToChannels.get(messageName) || messageToChannels.get(name) || [];
288
+ messageSchemas.push({
289
+ name: messageName,
290
+ rustName: toRustTypeName(messageName),
291
+ payload,
292
+ rawPayload: null,
293
+ description: description || title,
294
+ channels
295
+ });
296
+ }
297
+ });
298
+ }
299
+ }
300
+
301
+ // Helper function to convert JSON schema to Rust type
302
+ function jsonSchemaToRustType(schema, typeName = null) {
303
+ if (!schema) return 'serde_json::Value';
304
+
305
+ // Handle $ref
306
+ if (schema.$ref) {
307
+ const refName = schema.$ref.split('/').pop();
308
+ const rustTypeName = toRustTypeName(refName);
309
+
310
+ // Generate the referenced schema if we have access to components
311
+ if (components && components.schemas) {
312
+ const schemas = components.schemas();
313
+ if (schemas && schemas[refName] && !generatedTypes.has(rustTypeName)) {
314
+ generatedTypes.add(rustTypeName);
315
+ const referencedSchema = schemas[refName];
316
+ const schemaJson = referencedSchema.json ? referencedSchema.json() : referencedSchema;
317
+ nestedSchemas.set(rustTypeName, {
318
+ type: 'struct',
319
+ schema: schemaJson,
320
+ description: referencedSchema.description && typeof referencedSchema.description === 'function' ? referencedSchema.description() : null
321
+ });
322
+ }
323
+ }
324
+
325
+ return rustTypeName;
326
+ }
327
+
328
+ // Handle resolved $ref - check for x-parser-schema-id which indicates original schema name
329
+ if (schema['x-parser-schema-id'] && typeof schema['x-parser-schema-id'] === 'string') {
330
+ const schemaId = schema['x-parser-schema-id'];
331
+ // Check if this matches a known component schema
332
+ if (schemaRegistry.has(schemaId)) {
333
+ const rustTypeName = toRustTypeName(schemaId);
334
+ return rustTypeName;
335
+ }
336
+ }
337
+
338
+ if (!schema.type) {
339
+ // If no type specified, check for properties (object) or items (array)
340
+ if (schema.properties) {
341
+ schema.type = 'object';
342
+ } else if (schema.items) {
343
+ schema.type = 'array';
344
+ } else {
345
+ return 'serde_json::Value';
346
+ }
347
+ }
348
+
349
+ switch (schema.type) {
350
+ case 'string':
351
+ if (schema.enum && schema.enum.length > 0) {
352
+ // Generate enum type
353
+ if (typeName) {
354
+ const enumName = `${typeName}Enum`;
355
+ if (!generatedTypes.has(enumName)) {
356
+ generatedTypes.add(enumName);
357
+ nestedSchemas.set(enumName, {
358
+ type: 'enum',
359
+ variants: schema.enum,
360
+ description: schema.description
361
+ });
362
+ }
363
+ return enumName;
364
+ }
365
+ return 'String'; // Fallback if no type name provided
366
+ }
367
+ if (schema.format === 'date-time') return 'chrono::DateTime<chrono::Utc>';
368
+ if (schema.format === 'uuid') return 'uuid::Uuid';
369
+ if (schema.format === 'email') return 'String';
370
+ if (schema.format === 'uri') return 'String';
371
+ return 'String';
372
+ case 'integer':
373
+ switch (schema.format) {
374
+ case 'int32':
375
+ return 'i32';
376
+ case 'int64':
377
+ return 'i64';
378
+ case 'uint32':
379
+ return 'u32';
380
+ case 'uint64':
381
+ return 'u64';
382
+ default:
383
+ // Default to i32 for unspecified format (maintains backward compatibility)
384
+ return 'i32';
385
+ }
386
+ case 'number':
387
+ return 'f64';
388
+ case 'boolean':
389
+ return 'bool';
390
+ case 'array': {
391
+ const itemType = jsonSchemaToRustType(schema.items);
392
+ return `Vec<${itemType}>`;
393
+ }
394
+ case 'object':
395
+ if (schema.properties && Object.keys(schema.properties).length > 0) {
396
+ // Generate nested struct
397
+ if (typeName) {
398
+ const structName = toRustTypeName(typeName);
399
+ if (!generatedTypes.has(structName)) {
400
+ generatedTypes.add(structName);
401
+ nestedSchemas.set(structName, {
402
+ type: 'struct',
403
+ schema: schema,
404
+ description: schema.description
405
+ });
406
+ }
407
+ return structName;
408
+ }
409
+ }
410
+ return 'serde_json::Value';
411
+ default:
412
+ return 'serde_json::Value';
413
+ }
414
+ }
415
+
416
+ // Generate message structs
417
+ function generateMessageStruct(schema, messageName) {
418
+ if (!schema || !schema.properties) {
419
+ return ' pub data: serde_json::Value,';
420
+ }
421
+
422
+ const fields = Object.entries(schema.properties).map(([fieldName, fieldSchema]) => {
423
+ const rustFieldName = toRustFieldName(fieldName);
424
+ const fieldTypeName = `${messageName}${toRustTypeName(fieldName)}`;
425
+ const rustType = jsonSchemaToRustType(fieldSchema, fieldTypeName);
426
+ const requiredFields = schema.required;
427
+ const optional = !requiredFields || !Array.isArray(requiredFields) || requiredFields.indexOf(fieldName) === -1;
428
+ const finalType = optional ? `Option<${rustType}>` : rustType;
429
+
430
+ let fieldDoc = '';
431
+ if (fieldSchema.description) {
432
+ fieldDoc = ` /// ${fieldSchema.description}\n`;
433
+ }
434
+
435
+ let serdeRename = '';
436
+ if (rustFieldName !== fieldName) {
437
+ serdeRename = ` #[serde(rename = "${fieldName}")]\n`;
438
+ }
439
+
440
+ let skipSerializing = '';
441
+ if (optional) {
442
+ skipSerializing = ' #[serde(skip_serializing_if = "Option::is_none")]\n';
443
+ }
444
+
445
+ return `${fieldDoc}${serdeRename}${skipSerializing} pub ${rustFieldName}: ${finalType},`;
446
+ }).join('\n');
447
+
448
+ return fields;
449
+ }
450
+
451
+ // Process all component schemas first to ensure they are available for references
452
+ componentSchemas.forEach(schema => {
453
+ jsonSchemaToRustType(schema.schema, schema.rustName);
454
+ generatedTypes.add(schema.rustName);
455
+ });
456
+
457
+ // Process all message schemas to ensure all referenced types are generated
458
+ messageSchemas.forEach(schema => {
459
+ if (schema.payload) {
460
+ jsonSchemaToRustType(schema.payload, schema.rustName);
461
+ }
462
+ });
463
+
464
+ // Generate component schema definitions
465
+ function generateComponentSchemas() {
466
+ let result = '';
467
+
468
+ componentSchemas.forEach(schema => {
469
+ const doc = schema.description ? `/// ${schema.description}\n` : `/// ${schema.name}\n`;
470
+
471
+ // Check if this is a standalone enum schema
472
+ if (schema.schema.type === 'string' && schema.schema.enum && Array.isArray(schema.schema.enum)) {
473
+ // Generate enum definition with serde rename attributes for lowercase serialization
474
+ const variants = schema.schema.enum.map(variant => {
475
+ const { rustName, serializedName } = toRustEnumVariantWithSerde(variant);
476
+ return ` #[serde(rename = "${serializedName}")]\n ${rustName}`;
477
+ }).join(',\n');
478
+ result += `
479
+ ${doc}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
480
+ pub enum ${schema.rustName} {
481
+ ${variants},
482
+ }
483
+ `;
484
+ } else {
485
+ // Generate struct definition
486
+ const fields = generateMessageStruct(schema.schema, schema.rustName);
487
+ result += `
488
+ ${doc}#[derive(Debug, Clone, Serialize, Deserialize)]
489
+ pub struct ${schema.rustName} {
490
+ ${fields}
491
+ }
492
+ `;
493
+ }
494
+ });
495
+
496
+ return result;
497
+ }
498
+
499
+ // Generate nested type definitions
500
+ function generateNestedTypes() {
501
+ let result = '';
502
+
503
+ for (const [typeName, typeInfo] of nestedSchemas.entries()) {
504
+ // Don't skip enums - they need to be generated even if the parent type exists
505
+ const isEnum = typeInfo.type === 'enum';
506
+ const isComponentSchema = componentSchemas.some(cs => cs.rustName === typeName);
507
+
508
+ // Skip if this type was already generated as a component schema (but not enums)
509
+ if (!isEnum && isComponentSchema) {
510
+ continue;
511
+ }
512
+
513
+ if (typeInfo.type === 'enum') {
514
+ const variants = typeInfo.variants.map(variant => {
515
+ const { rustName, serializedName } = toRustEnumVariantWithSerde(variant);
516
+ return ` #[serde(rename = "${serializedName}")]\n ${rustName}`;
517
+ }).join(',\n');
518
+ const doc = typeInfo.description ? `/// ${typeInfo.description}\n` : '';
519
+ result += `
520
+ ${doc}#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
521
+ pub enum ${typeName} {
522
+ ${variants},
523
+ }
524
+ `;
525
+ } else if (typeInfo.type === 'struct') {
526
+ const fields = generateMessageStruct(typeInfo.schema, typeName);
527
+ const doc = typeInfo.description ? `/// ${typeInfo.description}\n` : '';
528
+ result += `
529
+ ${doc}#[derive(Debug, Clone, Serialize, Deserialize)]
530
+ pub struct ${typeName} {
531
+ ${fields}
532
+ }
533
+ `;
534
+ }
535
+ }
536
+
537
+ return result;
538
+ }
539
+
540
+ return (
541
+ <File name="models.rs">
542
+ {`//! Generated data models from AsyncAPI specification
543
+
544
+ use serde::{Deserialize, Serialize};
545
+
546
+ ${generateComponentSchemas()}
547
+ ${generateNestedTypes()}
548
+ ${(() => {
549
+ // Track which types have already had implementations generated
550
+ const implementedTypes = new Set();
551
+ const implementations = [];
552
+
553
+ messageSchemas.forEach(schema => {
554
+ const doc = schema.description ? `/// ${schema.description}` : `/// ${schema.name} message`;
555
+
556
+ // Check if the message payload references a component schema
557
+ let payloadRustName = null;
558
+ let isComponentMessage = false;
559
+
560
+ if (schema.rawPayload && schema.rawPayload.$ref) {
561
+ const refName = schema.rawPayload.$ref.split('/').pop();
562
+ payloadRustName = toRustTypeName(refName);
563
+ isComponentMessage = true;
564
+ } else if (schema.payload && schema.payload.$ref) {
565
+ const refName = schema.payload.$ref.split('/').pop();
566
+ payloadRustName = toRustTypeName(refName);
567
+ isComponentMessage = true;
568
+ } else if (schema.payload && schema.payload['x-parser-schema-id']) {
569
+ // Handle resolved $ref references
570
+ const schemaId = schema.payload['x-parser-schema-id'];
571
+ if (schemaRegistry.has(schemaId)) {
572
+ payloadRustName = toRustTypeName(schemaId);
573
+ isComponentMessage = true;
574
+ }
575
+ }
576
+
577
+ // For component messages, always generate the message wrapper type
578
+ // even if the payload schema already exists
579
+ if (isComponentMessage && payloadRustName && !implementedTypes.has(schema.rustName)) {
580
+ implementedTypes.add(schema.rustName);
581
+ implementations.push(`
582
+ ${doc}
583
+ #[derive(Debug, Clone, Serialize, Deserialize)]
584
+ pub struct ${schema.rustName} {
585
+ #[serde(flatten)]
586
+ pub payload: ${payloadRustName},
587
+ }`);
588
+ } else if (!generatedTypes.has(schema.rustName) && !implementedTypes.has(schema.rustName)) {
589
+ // Generate both struct for inline message schemas
590
+ implementedTypes.add(schema.rustName);
591
+ implementations.push(`
592
+ ${doc}
593
+ #[derive(Debug, Clone, Serialize, Deserialize)]
594
+ pub struct ${schema.rustName} {
595
+ ${generateMessageStruct(schema.payload, schema.rustName)}
596
+ }`);
597
+ }
598
+ });
599
+
600
+ return implementations.join('');
601
+ })()}
602
+
603
+ ${messageSchemas.length === 0 && componentSchemas.length === 0 ? `
604
+ /// Example message structure when no messages are defined in the spec
605
+ #[derive(Debug, Clone, Serialize, Deserialize)]
606
+ pub struct ExampleMessage {
607
+ pub id: String,
608
+ pub content: String,
609
+ pub timestamp: chrono::DateTime<chrono::Utc>,
610
+ }
611
+
612
+ impl ExampleMessage {
613
+ /// Create a new instance with required fields
614
+ pub fn new(id: String, content: String, timestamp: chrono::DateTime<chrono::Utc>) -> Self {
615
+ Self {
616
+ id,
617
+ content,
618
+ timestamp,
619
+ }
620
+ }
621
+ }` : ''}
622
+ `}
623
+ </File>
624
+ );
625
+ }