@ioka-technologies/asyncapi-rust-client-template 0.0.21 → 0.0.23

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.
@@ -5,552 +5,37 @@ import {
5
5
  toRustTypeName,
6
6
  toRustFieldName,
7
7
  toRustEnumVariant,
8
- toRustEnumVariantWithSerde
9
- } from '../helpers/index.js';
8
+ toRustEnumVariantWithSerde,
9
+ generateRustModels,
10
+ generateMessageEnvelope
11
+ } from '../../../common/src/index.js';
10
12
 
11
13
  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
- }
14
+ // Generate models using the common helper
15
+ const models = generateRustModels(asyncapi, {
16
+ toRustTypeName,
17
+ toRustFieldName,
18
+ toRustEnumVariantWithSerde,
19
+ includeAsyncApiTrait: false, // Client doesn't need the AsyncApiMessage trait
20
+ includeEnvelope: false // We'll generate the envelope separately
462
21
  });
463
22
 
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
- }
23
+ // Generate the unified message envelope
24
+ const envelopeCode = generateMessageEnvelope();
539
25
 
540
26
  return (
541
27
  <File name="models.rs">
542
28
  {`//! Generated data models from AsyncAPI specification
543
29
 
544
- use serde::{Deserialize, Serialize};
545
-
546
- ${generateComponentSchemas()}
547
- ${generateNestedTypes()}
30
+ ${envelopeCode}
31
+ ${models.generateComponentSchemas()}
32
+ ${models.generateNestedTypes()}
548
33
  ${(() => {
549
34
  // Track which types have already had implementations generated
550
35
  const implementedTypes = new Set();
551
36
  const implementations = [];
552
37
 
553
- messageSchemas.forEach(schema => {
38
+ models.messageSchemas.forEach(schema => {
554
39
  const doc = schema.description ? `/// ${schema.description}` : `/// ${schema.name} message`;
555
40
 
556
41
  // Check if the message payload references a component schema
@@ -568,7 +53,7 @@ ${(() => {
568
53
  } else if (schema.payload && schema.payload['x-parser-schema-id']) {
569
54
  // Handle resolved $ref references
570
55
  const schemaId = schema.payload['x-parser-schema-id'];
571
- if (schemaRegistry.has(schemaId)) {
56
+ if (models.schemaRegistry.has(schemaId)) {
572
57
  payloadRustName = toRustTypeName(schemaId);
573
58
  isComponentMessage = true;
574
59
  }
@@ -585,14 +70,14 @@ pub struct ${schema.rustName} {
585
70
  #[serde(flatten)]
586
71
  pub payload: ${payloadRustName},
587
72
  }`);
588
- } else if (!generatedTypes.has(schema.rustName) && !implementedTypes.has(schema.rustName)) {
73
+ } else if (!models.generatedTypes.has(schema.rustName) && !implementedTypes.has(schema.rustName)) {
589
74
  // Generate both struct for inline message schemas
590
75
  implementedTypes.add(schema.rustName);
591
76
  implementations.push(`
592
77
  ${doc}
593
78
  #[derive(Debug, Clone, Serialize, Deserialize)]
594
79
  pub struct ${schema.rustName} {
595
- ${generateMessageStruct(schema.payload, schema.rustName)}
80
+ ${models.generateMessageStruct(schema.payload, schema.rustName)}
596
81
  }`);
597
82
  }
598
83
  });
@@ -600,7 +85,7 @@ ${generateMessageStruct(schema.payload, schema.rustName)}
600
85
  return implementations.join('');
601
86
  })()}
602
87
 
603
- ${messageSchemas.length === 0 && componentSchemas.length === 0 ? `
88
+ ${models.messageSchemas.length === 0 && models.componentSchemas.length === 0 ? `
604
89
  /// Example message structure when no messages are defined in the spec
605
90
  #[derive(Debug, Clone, Serialize, Deserialize)]
606
91
  pub struct ExampleMessage {
@@ -1,48 +0,0 @@
1
- {
2
- "name": "@asyncapi/rust-client-template",
3
- "version": "0.0.0",
4
- "description": "AsyncAPI template for generating Rust NATS clients",
5
- "main": "index.js",
6
- "keywords": [
7
- "asyncapi",
8
- "template",
9
- "rust",
10
- "nats",
11
- "client",
12
- "generator"
13
- ],
14
- "author": "AsyncAPI Generator",
15
- "license": "Apache-2.0",
16
- "dependencies": {
17
- "@asyncapi/generator-react-sdk": "^1.0.0"
18
- },
19
- "generator": {
20
- "renderer": "react",
21
- "apiVersion": "v3",
22
- "supportedProtocols": [
23
- "nats"
24
- ],
25
- "parameters": {
26
- "clientName": {
27
- "description": "Name of the generated client struct",
28
- "default": "{{asyncapi.info().title() | replace(/[^a-zA-Z0-9]/g, '') }}Client"
29
- },
30
- "packageName": {
31
- "description": "Name of the generated Rust crate",
32
- "default": "{{asyncapi.info().title() | kebabCase}}-client"
33
- },
34
- "packageVersion": {
35
- "description": "Version of the generated crate",
36
- "default": "{{asyncapi.info().version()}}"
37
- },
38
- "author": {
39
- "description": "Author of the generated crate",
40
- "default": "AsyncAPI Generator"
41
- },
42
- "license": {
43
- "description": "License of the generated crate",
44
- "default": "Apache-2.0"
45
- }
46
- }
47
- }
48
- }