@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.
- package/README.md +262 -0
- package/package.json +52 -0
- package/template/Cargo.toml.js +100 -0
- package/template/helpers/index.js +501 -0
- package/template/index.js +244 -0
- package/template/package.json +48 -0
- package/template/src/auth.rs.js +221 -0
- package/template/src/client.rs.js +681 -0
- package/template/src/envelope.rs.js +199 -0
- package/template/src/errors.rs.js +49 -0
- package/template/src/lib.rs.js +112 -0
- package/template/src/models.rs.js +625 -0
|
@@ -0,0 +1,681 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
import {
|
|
4
|
+
toRustIdentifier,
|
|
5
|
+
toRustTypeName,
|
|
6
|
+
toRustFieldName,
|
|
7
|
+
getPayloadRustTypeName,
|
|
8
|
+
analyzeClientOperations,
|
|
9
|
+
getNatsSubject,
|
|
10
|
+
isDynamicChannel,
|
|
11
|
+
extractChannelVariables,
|
|
12
|
+
getChannelParameters,
|
|
13
|
+
resolveChannelAddress,
|
|
14
|
+
channelHasParameters
|
|
15
|
+
} from '../helpers/index.js';
|
|
16
|
+
|
|
17
|
+
export default function ClientRs({ asyncapi, params }) {
|
|
18
|
+
const info = asyncapi.info();
|
|
19
|
+
const title = info.title();
|
|
20
|
+
|
|
21
|
+
// Helper function to check if a parameter contains unresolved template variables
|
|
22
|
+
function isTemplateVariable(value) {
|
|
23
|
+
return typeof value === 'string' && value.includes('{{') && value.includes('}}');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Helper function to convert title to PascalCase for struct names
|
|
27
|
+
function toPascalCase(str) {
|
|
28
|
+
return str.replace(/[^a-zA-Z0-9]/g, ' ')
|
|
29
|
+
.split(' ')
|
|
30
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
31
|
+
.join('');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Resolve clientName parameter, falling back to extracted values if parameter contains template variables
|
|
35
|
+
const clientName = (params.clientName && !isTemplateVariable(params.clientName))
|
|
36
|
+
? params.clientName
|
|
37
|
+
: `${toPascalCase(title)}Client`;
|
|
38
|
+
|
|
39
|
+
// Extract all operations from the AsyncAPI spec
|
|
40
|
+
const allOperations = [];
|
|
41
|
+
const processedOperations = new Set(); // Track processed operations to avoid duplicates
|
|
42
|
+
|
|
43
|
+
// Skip processing operations from the operations collection since we'll get them from channels
|
|
44
|
+
// This avoids duplicate operations and ensures we have proper channel context
|
|
45
|
+
|
|
46
|
+
// Extract channels and their operations (AsyncAPI 3.x style)
|
|
47
|
+
const channelServices = [];
|
|
48
|
+
if (asyncapi.channels) {
|
|
49
|
+
const channels = asyncapi.channels();
|
|
50
|
+
if (channels) {
|
|
51
|
+
for (const channel of channels) {
|
|
52
|
+
try {
|
|
53
|
+
const channelName = channel.id();
|
|
54
|
+
const subject = getNatsSubject(channel);
|
|
55
|
+
const isDynamic = isDynamicChannel(subject);
|
|
56
|
+
const parameters = isDynamic ? getChannelParameters(channel) : [];
|
|
57
|
+
|
|
58
|
+
// Create channel service info
|
|
59
|
+
const channelService = {
|
|
60
|
+
channelName,
|
|
61
|
+
subject,
|
|
62
|
+
isDynamic,
|
|
63
|
+
parameters,
|
|
64
|
+
operations: []
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
if (channel.operations) {
|
|
68
|
+
const operations = channel.operations();
|
|
69
|
+
if (operations) {
|
|
70
|
+
for (const operation of operations) {
|
|
71
|
+
try {
|
|
72
|
+
const operationId = operation.id ? operation.id() : null;
|
|
73
|
+
if (!operationId) continue;
|
|
74
|
+
|
|
75
|
+
const action = operation.action ? operation.action() : 'send';
|
|
76
|
+
const messages = operation.messages ? operation.messages() : null;
|
|
77
|
+
|
|
78
|
+
if (!messages || !messages.all) continue;
|
|
79
|
+
|
|
80
|
+
const messageArray = messages.all();
|
|
81
|
+
if (!messageArray || messageArray.length === 0) continue;
|
|
82
|
+
|
|
83
|
+
const firstMessage = messageArray[0];
|
|
84
|
+
const methodName = toRustFieldName(operationId);
|
|
85
|
+
|
|
86
|
+
if (action === 'send') {
|
|
87
|
+
const reply = operation.reply ? operation.reply() : null;
|
|
88
|
+
if (reply) {
|
|
89
|
+
const replyMessages = reply.messages ? reply.messages() : null;
|
|
90
|
+
if (replyMessages && replyMessages.all) {
|
|
91
|
+
const replyMessageArray = replyMessages.all();
|
|
92
|
+
if (replyMessageArray && replyMessageArray.length > 0) {
|
|
93
|
+
const opInfo = {
|
|
94
|
+
type: 'request_reply',
|
|
95
|
+
operationName: operationId,
|
|
96
|
+
methodName,
|
|
97
|
+
requestType: getPayloadRustTypeName(firstMessage),
|
|
98
|
+
responseType: getPayloadRustTypeName(replyMessageArray[0]),
|
|
99
|
+
subject,
|
|
100
|
+
channelName
|
|
101
|
+
};
|
|
102
|
+
channelService.operations.push(opInfo);
|
|
103
|
+
allOperations.push(opInfo);
|
|
104
|
+
processedOperations.add(operationId);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const opInfo = {
|
|
111
|
+
type: 'publish',
|
|
112
|
+
operationName: operationId,
|
|
113
|
+
methodName,
|
|
114
|
+
payloadType: getPayloadRustTypeName(firstMessage),
|
|
115
|
+
subject,
|
|
116
|
+
channelName
|
|
117
|
+
};
|
|
118
|
+
channelService.operations.push(opInfo);
|
|
119
|
+
allOperations.push(opInfo);
|
|
120
|
+
processedOperations.add(operationId);
|
|
121
|
+
|
|
122
|
+
// Also add a subscription method for notification-type operations
|
|
123
|
+
// (send operations without replies are typically notifications/events)
|
|
124
|
+
const subscribeOpInfo = {
|
|
125
|
+
type: 'subscribe',
|
|
126
|
+
operationName: operationId,
|
|
127
|
+
methodName: `subscribe_to_${methodName}`,
|
|
128
|
+
payloadType: getPayloadRustTypeName(firstMessage),
|
|
129
|
+
subject,
|
|
130
|
+
channelName
|
|
131
|
+
};
|
|
132
|
+
channelService.operations.push(subscribeOpInfo);
|
|
133
|
+
allOperations.push(subscribeOpInfo);
|
|
134
|
+
} else if (action === 'receive') {
|
|
135
|
+
const opInfo = {
|
|
136
|
+
type: 'subscribe',
|
|
137
|
+
operationName: operationId,
|
|
138
|
+
methodName: toRustFieldName(operationId.replace(/^receive/, 'subscribeTo')),
|
|
139
|
+
payloadType: getPayloadRustTypeName(firstMessage),
|
|
140
|
+
subject,
|
|
141
|
+
channelName
|
|
142
|
+
};
|
|
143
|
+
channelService.operations.push(opInfo);
|
|
144
|
+
allOperations.push(opInfo);
|
|
145
|
+
processedOperations.add(operationId);
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
console.warn(`Error processing channel operation: ${e.message}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (channelService.operations.length > 0) {
|
|
155
|
+
channelServices.push(channelService);
|
|
156
|
+
}
|
|
157
|
+
} catch (e) {
|
|
158
|
+
console.warn(`Error processing channel: ${e.message}`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Generate channel service structs for dynamic channels
|
|
165
|
+
function generateChannelServices() {
|
|
166
|
+
const dynamicChannels = channelServices.filter(cs => cs.isDynamic);
|
|
167
|
+
if (dynamicChannels.length === 0) {
|
|
168
|
+
return '';
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return dynamicChannels.map(channelService => {
|
|
172
|
+
const serviceName = `${toPascalCase(channelService.channelName)}Service`;
|
|
173
|
+
|
|
174
|
+
// Extract variables from the channel address to get the actual parameter names
|
|
175
|
+
const variables = extractChannelVariables(channelService.subject);
|
|
176
|
+
const actualParams = variables.map(varName => ({
|
|
177
|
+
name: varName,
|
|
178
|
+
rustName: toRustFieldName(varName),
|
|
179
|
+
description: channelService.parameters.find(p => p.name === varName)?.description || `${varName} parameter`
|
|
180
|
+
}));
|
|
181
|
+
|
|
182
|
+
const paramSignature = actualParams.map(p => `${p.rustName}: &str`).join(', ');
|
|
183
|
+
const paramArgs = actualParams.map(p => `${p.rustName}: ${p.rustName}.to_string()`).join(', ');
|
|
184
|
+
|
|
185
|
+
const serviceOperations = channelService.operations.map(op => {
|
|
186
|
+
const resolvedSubject = 'resolved_subject';
|
|
187
|
+
|
|
188
|
+
switch (op.type) {
|
|
189
|
+
case 'request_reply':
|
|
190
|
+
return ` /// ${op.operationName} - Request/Reply operation
|
|
191
|
+
///
|
|
192
|
+
/// Sends a request and waits for a response using NATS request/reply pattern.
|
|
193
|
+
/// This operation uses the NATS Services API for reliable request/response messaging.
|
|
194
|
+
///
|
|
195
|
+
/// # Arguments
|
|
196
|
+
/// * \`payload\` - Request payload
|
|
197
|
+
///
|
|
198
|
+
/// # Returns
|
|
199
|
+
/// * \`ClientResult<${op.responseType}>\` - The response from the server
|
|
200
|
+
///
|
|
201
|
+
/// # Errors
|
|
202
|
+
/// * \`ClientError::Nats\` - NATS operation failed
|
|
203
|
+
/// * \`ClientError::Serialization\` - Failed to serialize/deserialize data
|
|
204
|
+
/// * \`ClientError::Timeout\` - Request timed out
|
|
205
|
+
pub async fn ${op.methodName}(&self, payload: ${op.requestType}) -> ClientResult<${op.responseType}> {
|
|
206
|
+
let envelope = if let Some(ref auth) = self.auth {
|
|
207
|
+
MessageEnvelope::new_with_auth("${op.operationName}", payload, auth)
|
|
208
|
+
.map_err(ClientError::Serialization)?
|
|
209
|
+
} else {
|
|
210
|
+
MessageEnvelope::new("${op.operationName}", payload)
|
|
211
|
+
.map_err(ClientError::Serialization)?
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
let subject = self.${resolvedSubject}.clone();
|
|
215
|
+
let response = self.client
|
|
216
|
+
.request(subject, Bytes::from(envelope.to_bytes()?))
|
|
217
|
+
.await
|
|
218
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
219
|
+
|
|
220
|
+
let response_envelope = MessageEnvelope::from_bytes(&response.payload)
|
|
221
|
+
.map_err(|e| ClientError::InvalidEnvelope(e.to_string()))?;
|
|
222
|
+
|
|
223
|
+
let result: ${op.responseType} = response_envelope.extract_payload()
|
|
224
|
+
.map_err(ClientError::Serialization)?;
|
|
225
|
+
|
|
226
|
+
Ok(result)
|
|
227
|
+
}`;
|
|
228
|
+
|
|
229
|
+
case 'publish':
|
|
230
|
+
return ` /// ${op.operationName} - Publish operation
|
|
231
|
+
///
|
|
232
|
+
/// Publishes a message using NATS publish (fire-and-forget).
|
|
233
|
+
/// This is a one-way operation with no response expected.
|
|
234
|
+
///
|
|
235
|
+
/// # Arguments
|
|
236
|
+
/// * \`payload\` - Message payload to publish
|
|
237
|
+
///
|
|
238
|
+
/// # Errors
|
|
239
|
+
/// * \`ClientError::Nats\` - NATS operation failed
|
|
240
|
+
/// * \`ClientError::Serialization\` - Failed to serialize data
|
|
241
|
+
pub async fn ${op.methodName}(&self, payload: ${op.payloadType}) -> ClientResult<()> {
|
|
242
|
+
let envelope = if let Some(ref auth) = self.auth {
|
|
243
|
+
MessageEnvelope::new_with_auth("${op.operationName}", payload, auth)
|
|
244
|
+
.map_err(ClientError::Serialization)?
|
|
245
|
+
} else {
|
|
246
|
+
MessageEnvelope::new("${op.operationName}", payload)
|
|
247
|
+
.map_err(ClientError::Serialization)?
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
let subject = self.${resolvedSubject}.clone();
|
|
251
|
+
self.client
|
|
252
|
+
.publish(subject, Bytes::from(envelope.to_bytes()?))
|
|
253
|
+
.await
|
|
254
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
255
|
+
|
|
256
|
+
Ok(())
|
|
257
|
+
}`;
|
|
258
|
+
|
|
259
|
+
case 'subscribe':
|
|
260
|
+
return ` /// ${op.operationName} - Subscribe operation
|
|
261
|
+
///
|
|
262
|
+
/// Creates a subscription to receive messages from the specified subject.
|
|
263
|
+
/// Returns a NATS subscriber that can be used to receive messages.
|
|
264
|
+
///
|
|
265
|
+
/// # Returns
|
|
266
|
+
/// * \`ClientResult<async_nats::Subscriber>\` - NATS subscriber for handling messages
|
|
267
|
+
///
|
|
268
|
+
/// # Example
|
|
269
|
+
/// \`\`\`no-run
|
|
270
|
+
/// let mut subscriber = service.${op.methodName}().await?;
|
|
271
|
+
/// while let Some(message) = subscriber.next().await {
|
|
272
|
+
/// let envelope = MessageEnvelope::from_bytes(&message.payload)?;
|
|
273
|
+
/// let payload: ${op.payloadType} = envelope.extract_payload()?;
|
|
274
|
+
/// // Handle the message...
|
|
275
|
+
/// }
|
|
276
|
+
/// \`\`\`
|
|
277
|
+
pub async fn ${op.methodName}(&self) -> ClientResult<async_nats::Subscriber> {
|
|
278
|
+
let subject = self.${resolvedSubject}.clone();
|
|
279
|
+
let subscriber = self.client
|
|
280
|
+
.subscribe(subject)
|
|
281
|
+
.await
|
|
282
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
283
|
+
|
|
284
|
+
Ok(subscriber)
|
|
285
|
+
}`;
|
|
286
|
+
|
|
287
|
+
default:
|
|
288
|
+
return '';
|
|
289
|
+
}
|
|
290
|
+
}).filter(method => method).join('\n\n');
|
|
291
|
+
|
|
292
|
+
return `
|
|
293
|
+
/// ${channelService.channelName} channel service with resolved parameters
|
|
294
|
+
///
|
|
295
|
+
/// This service provides access to operations on the ${channelService.channelName} channel
|
|
296
|
+
/// with resolved channel parameters for dynamic routing.
|
|
297
|
+
#[derive(Debug, Clone)]
|
|
298
|
+
pub struct ${serviceName} {
|
|
299
|
+
client: async_nats::Client,
|
|
300
|
+
resolved_subject: String,
|
|
301
|
+
auth: Option<AuthCredentials>,
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
impl ${serviceName} {
|
|
305
|
+
/// Create a new ${channelService.channelName} service with resolved parameters
|
|
306
|
+
///
|
|
307
|
+
/// # Arguments
|
|
308
|
+
${actualParams.map(p => ` /// * \`${p.rustName}\` - ${p.description}`).join('\n')}
|
|
309
|
+
pub fn new(client: async_nats::Client, ${paramSignature}) -> Self {
|
|
310
|
+
let resolved_subject = "${channelService.subject}".to_string()${actualParams.map((p, index) => {
|
|
311
|
+
return `
|
|
312
|
+
.replace("{${p.name}}", ${p.rustName})`;
|
|
313
|
+
}).join('')};
|
|
314
|
+
|
|
315
|
+
Self {
|
|
316
|
+
client,
|
|
317
|
+
resolved_subject,
|
|
318
|
+
auth: None,
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/// Get the resolved subject for this channel service
|
|
323
|
+
pub fn subject(&self) -> &str {
|
|
324
|
+
&self.resolved_subject
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/// Create a new service with authentication credentials
|
|
328
|
+
pub fn with_auth(mut self, auth: AuthCredentials) -> Self {
|
|
329
|
+
self.auth = Some(auth);
|
|
330
|
+
self
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/// Update authentication credentials
|
|
334
|
+
pub fn update_auth(&mut self, auth: AuthCredentials) {
|
|
335
|
+
self.auth = Some(auth);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
/// Remove authentication credentials
|
|
339
|
+
pub fn clear_auth(&mut self) {
|
|
340
|
+
self.auth = None;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/// Check if authentication credentials are configured
|
|
344
|
+
pub fn has_auth(&self) -> bool {
|
|
345
|
+
self.auth.as_ref().map_or(false, |auth| auth.has_credentials())
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
${serviceOperations}
|
|
349
|
+
}`;
|
|
350
|
+
}).join('\n');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
// Generate client methods for static channels and channel service accessors for dynamic channels
|
|
354
|
+
function generateClientMethods() {
|
|
355
|
+
const staticOperations = allOperations.filter(op => {
|
|
356
|
+
const channelService = channelServices.find(cs => cs.channelName === op.channelName);
|
|
357
|
+
return !channelService || !channelService.isDynamic;
|
|
358
|
+
});
|
|
359
|
+
|
|
360
|
+
const dynamicChannelAccessors = channelServices.filter(cs => cs.isDynamic).map(channelService => {
|
|
361
|
+
const serviceName = `${toPascalCase(channelService.channelName)}Service`;
|
|
362
|
+
|
|
363
|
+
// Extract variables from the channel address to get the actual parameter names
|
|
364
|
+
const variables = extractChannelVariables(channelService.subject);
|
|
365
|
+
const actualParams = variables.map(varName => ({
|
|
366
|
+
name: varName,
|
|
367
|
+
rustName: toRustFieldName(varName),
|
|
368
|
+
description: channelService.parameters.find(p => p.name === varName)?.description || `${varName} parameter`
|
|
369
|
+
}));
|
|
370
|
+
|
|
371
|
+
const paramSignature = actualParams.map(p => `${p.rustName}: &str`).join(', ');
|
|
372
|
+
|
|
373
|
+
return ` /// Access ${channelService.channelName} channel operations with parameters
|
|
374
|
+
///
|
|
375
|
+
/// Returns a service instance configured for the specific channel parameters.
|
|
376
|
+
///
|
|
377
|
+
/// # Arguments
|
|
378
|
+
${actualParams.map(p => ` /// * \`${p.rustName}\` - ${p.description}`).join('\n')}
|
|
379
|
+
///
|
|
380
|
+
/// # Example
|
|
381
|
+
/// \`\`\`ignore
|
|
382
|
+
/// let service = client.${toRustFieldName(channelService.channelName)}(${actualParams.map(p => `"${p.name.replace('_', '-')}"`).join(', ')});
|
|
383
|
+
/// let result = service.${channelService.operations[0]?.methodName || 'operation'}(payload).await?;
|
|
384
|
+
/// \`\`\`
|
|
385
|
+
pub fn ${toRustFieldName(channelService.channelName)}(&self, ${paramSignature}) -> ${serviceName} {
|
|
386
|
+
let mut service = ${serviceName}::new(self.client.clone(), ${actualParams.map(p => p.rustName).join(', ')});
|
|
387
|
+
if let Some(ref auth) = self.auth {
|
|
388
|
+
service.auth = Some(auth.clone());
|
|
389
|
+
}
|
|
390
|
+
service
|
|
391
|
+
}`;
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
const staticMethods = staticOperations.map(pattern => {
|
|
395
|
+
const subject = pattern.subject || pattern.channelName || 'unknown.subject';
|
|
396
|
+
|
|
397
|
+
switch (pattern.type) {
|
|
398
|
+
case 'request_reply':
|
|
399
|
+
return ` /// ${pattern.operationName} - Request/Reply operation
|
|
400
|
+
///
|
|
401
|
+
/// Sends a request and waits for a response using NATS request/reply pattern.
|
|
402
|
+
/// This operation uses the NATS Services API for reliable request/response messaging.
|
|
403
|
+
///
|
|
404
|
+
/// # Arguments
|
|
405
|
+
/// * \`payload\` - Request payload
|
|
406
|
+
///
|
|
407
|
+
/// # Returns
|
|
408
|
+
/// * \`ClientResult<${pattern.responseType}>\` - The response from the server
|
|
409
|
+
///
|
|
410
|
+
/// # Errors
|
|
411
|
+
/// * \`ClientError::Nats\` - NATS operation failed
|
|
412
|
+
/// * \`ClientError::Serialization\` - Failed to serialize/deserialize data
|
|
413
|
+
/// * \`ClientError::Timeout\` - Request timed out
|
|
414
|
+
pub async fn ${pattern.methodName}(&self, payload: ${pattern.requestType}) -> ClientResult<${pattern.responseType}> {
|
|
415
|
+
let envelope = if let Some(ref auth) = self.auth {
|
|
416
|
+
MessageEnvelope::new_with_auth("${pattern.operationName}", payload, auth)
|
|
417
|
+
.map_err(ClientError::Serialization)?
|
|
418
|
+
} else {
|
|
419
|
+
MessageEnvelope::new("${pattern.operationName}", payload)
|
|
420
|
+
.map_err(ClientError::Serialization)?
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
let response = self.client
|
|
424
|
+
.request("${subject}", Bytes::from(envelope.to_bytes()?))
|
|
425
|
+
.await
|
|
426
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
427
|
+
|
|
428
|
+
let response_envelope = MessageEnvelope::from_bytes(&response.payload)
|
|
429
|
+
.map_err(|e| ClientError::InvalidEnvelope(e.to_string()))?;
|
|
430
|
+
|
|
431
|
+
let result: ${pattern.responseType} = response_envelope.extract_payload()
|
|
432
|
+
.map_err(ClientError::Serialization)?;
|
|
433
|
+
|
|
434
|
+
Ok(result)
|
|
435
|
+
}`;
|
|
436
|
+
|
|
437
|
+
case 'publish':
|
|
438
|
+
return ` /// ${pattern.operationName} - Publish operation
|
|
439
|
+
///
|
|
440
|
+
/// Publishes a message using NATS publish (fire-and-forget).
|
|
441
|
+
/// This is a one-way operation with no response expected.
|
|
442
|
+
///
|
|
443
|
+
/// # Arguments
|
|
444
|
+
/// * \`payload\` - Message payload to publish
|
|
445
|
+
///
|
|
446
|
+
/// # Errors
|
|
447
|
+
/// * \`ClientError::Nats\` - NATS operation failed
|
|
448
|
+
/// * \`ClientError::Serialization\` - Failed to serialize data
|
|
449
|
+
pub async fn ${pattern.methodName}(&self, payload: ${pattern.payloadType}) -> ClientResult<()> {
|
|
450
|
+
let envelope = if let Some(ref auth) = self.auth {
|
|
451
|
+
MessageEnvelope::new_with_auth("${pattern.operationName}", payload, auth)
|
|
452
|
+
.map_err(ClientError::Serialization)?
|
|
453
|
+
} else {
|
|
454
|
+
MessageEnvelope::new("${pattern.operationName}", payload)
|
|
455
|
+
.map_err(ClientError::Serialization)?
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
self.client
|
|
459
|
+
.publish("${subject}", Bytes::from(envelope.to_bytes()?))
|
|
460
|
+
.await
|
|
461
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
462
|
+
|
|
463
|
+
Ok(())
|
|
464
|
+
}`;
|
|
465
|
+
|
|
466
|
+
case 'subscribe':
|
|
467
|
+
return ` /// ${pattern.operationName} - Subscribe operation
|
|
468
|
+
///
|
|
469
|
+
/// Creates a subscription to receive messages from the specified subject.
|
|
470
|
+
/// Returns a NATS subscriber that can be used to receive messages.
|
|
471
|
+
///
|
|
472
|
+
/// # Returns
|
|
473
|
+
/// * \`ClientResult<async_nats::Subscriber>\` - NATS subscriber for handling messages
|
|
474
|
+
///
|
|
475
|
+
/// # Example
|
|
476
|
+
/// \`\`\`ignore
|
|
477
|
+
/// let mut subscriber = client.${pattern.methodName}().await?;
|
|
478
|
+
/// while let Some(message) = subscriber.next().await {
|
|
479
|
+
/// let envelope = MessageEnvelope::from_bytes(&message.payload)?;
|
|
480
|
+
/// let payload: ${pattern.payloadType} = envelope.extract_payload()?;
|
|
481
|
+
/// // Handle the message...
|
|
482
|
+
/// }
|
|
483
|
+
/// \`\`\`
|
|
484
|
+
pub async fn ${pattern.methodName}(&self) -> ClientResult<async_nats::Subscriber> {
|
|
485
|
+
let subscriber = self.client
|
|
486
|
+
.subscribe("${subject}")
|
|
487
|
+
.await
|
|
488
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
489
|
+
|
|
490
|
+
Ok(subscriber)
|
|
491
|
+
}`;
|
|
492
|
+
|
|
493
|
+
default:
|
|
494
|
+
return '';
|
|
495
|
+
}
|
|
496
|
+
}).filter(method => method);
|
|
497
|
+
|
|
498
|
+
const allMethods = [...dynamicChannelAccessors, ...staticMethods];
|
|
499
|
+
|
|
500
|
+
if (allMethods.length === 0) {
|
|
501
|
+
return ` // No operations found in AsyncAPI specification
|
|
502
|
+
// You can add custom methods here`;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return allMethods.join('\n\n');
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
return (
|
|
509
|
+
<File name="client.rs">
|
|
510
|
+
{`//! Generated NATS client for ${title}
|
|
511
|
+
|
|
512
|
+
use crate::auth::AuthCredentials;
|
|
513
|
+
use crate::envelope::MessageEnvelope;
|
|
514
|
+
use crate::errors::{ClientError, ClientResult};
|
|
515
|
+
use crate::models::*;
|
|
516
|
+
use bytes::Bytes;
|
|
517
|
+
|
|
518
|
+
/// ${title} NATS client
|
|
519
|
+
///
|
|
520
|
+
/// This client provides type-safe access to ${title} operations via NATS messaging.
|
|
521
|
+
/// It supports request/reply, publish/subscribe, and other NATS patterns based on the AsyncAPI specification.
|
|
522
|
+
///
|
|
523
|
+
/// ## Features
|
|
524
|
+
///
|
|
525
|
+
/// - **Type Safety**: All operations use strongly-typed message structures
|
|
526
|
+
/// - **NATS Services API**: Request/reply operations use NATS Services for reliability
|
|
527
|
+
/// - **Message Envelopes**: All messages are wrapped in a standard envelope format
|
|
528
|
+
/// - **Error Handling**: Comprehensive error types for different failure modes
|
|
529
|
+
/// - **Async/Await**: Full async support with Tokio compatibility
|
|
530
|
+
/// - **Dynamic Channels**: Support for channels with variable parameters
|
|
531
|
+
///
|
|
532
|
+
/// ## Usage
|
|
533
|
+
///
|
|
534
|
+
/// \`\`\`ignore
|
|
535
|
+
/// use async_nats;
|
|
536
|
+
/// use your_crate::${clientName};
|
|
537
|
+
///
|
|
538
|
+
/// #[tokio::main]
|
|
539
|
+
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
540
|
+
/// // Connect to NATS server
|
|
541
|
+
/// let nats_client = async_nats::connect("nats://localhost:4222").await?;
|
|
542
|
+
///
|
|
543
|
+
/// // Create the AsyncAPI client
|
|
544
|
+
/// let client = ${clientName}::with(nats_client);
|
|
545
|
+
///
|
|
546
|
+
/// // For dynamic channels, use the channel service:
|
|
547
|
+
/// // let service = client.user_create("us-west-1");
|
|
548
|
+
/// // let result = service.create_user(payload).await?;
|
|
549
|
+
///
|
|
550
|
+
/// Ok(())
|
|
551
|
+
/// }
|
|
552
|
+
/// \`\`\`
|
|
553
|
+
#[derive(Debug, Clone)]
|
|
554
|
+
pub struct ${clientName} {
|
|
555
|
+
client: async_nats::Client,
|
|
556
|
+
auth: Option<AuthCredentials>,
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
impl ${clientName} {
|
|
560
|
+
/// Create a new client with an existing NATS client
|
|
561
|
+
///
|
|
562
|
+
/// # Arguments
|
|
563
|
+
/// * \`client\` - A connected async-nats::Client instance
|
|
564
|
+
///
|
|
565
|
+
/// # Example
|
|
566
|
+
/// \`\`\`ignore
|
|
567
|
+
/// use async_nats;
|
|
568
|
+
/// use your_crate::${clientName};
|
|
569
|
+
///
|
|
570
|
+
/// #[tokio::main]
|
|
571
|
+
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
572
|
+
/// let nats_client = async_nats::connect("nats://localhost:4222").await?;
|
|
573
|
+
/// let client = ${clientName}::with(nats_client);
|
|
574
|
+
/// Ok(())
|
|
575
|
+
/// }
|
|
576
|
+
/// \`\`\`
|
|
577
|
+
pub fn with(client: async_nats::Client) -> Self {
|
|
578
|
+
Self { client, auth: None }
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/// Create a new client by connecting to NATS server
|
|
582
|
+
///
|
|
583
|
+
/// # Arguments
|
|
584
|
+
/// * \`url\` - NATS server URL (e.g., "nats://localhost:4222")
|
|
585
|
+
///
|
|
586
|
+
/// # Example
|
|
587
|
+
/// \`\`\`ignore
|
|
588
|
+
/// use your_crate::${clientName};
|
|
589
|
+
///
|
|
590
|
+
/// #[tokio::main]
|
|
591
|
+
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
592
|
+
/// let client = ${clientName}::connect("nats://localhost:4222").await?;
|
|
593
|
+
/// Ok(())
|
|
594
|
+
/// }
|
|
595
|
+
/// \`\`\`
|
|
596
|
+
pub async fn connect(url: &str) -> ClientResult<Self> {
|
|
597
|
+
let client = async_nats::connect(url)
|
|
598
|
+
.await
|
|
599
|
+
.map_err(|e| ClientError::Nats(Box::new(e)))?;
|
|
600
|
+
Ok(Self::with(client))
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/// Get a reference to the underlying NATS client
|
|
604
|
+
///
|
|
605
|
+
/// This allows access to low-level NATS operations if needed.
|
|
606
|
+
pub fn nats_client(&self) -> &async_nats::Client {
|
|
607
|
+
&self.client
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/// Create a new client with authentication credentials
|
|
611
|
+
///
|
|
612
|
+
/// # Arguments
|
|
613
|
+
/// * \`client\` - A connected async-nats::Client instance
|
|
614
|
+
/// * \`auth\` - Authentication credentials
|
|
615
|
+
///
|
|
616
|
+
/// # Example
|
|
617
|
+
/// \`\`\`ignore
|
|
618
|
+
/// use async_nats;
|
|
619
|
+
/// use your_crate::{${clientName}, AuthCredentials};
|
|
620
|
+
///
|
|
621
|
+
/// #[tokio::main]
|
|
622
|
+
/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
623
|
+
/// let nats_client = async_nats::connect("nats://localhost:4222").await?;
|
|
624
|
+
/// let auth = AuthCredentials::jwt("your-jwt-token");
|
|
625
|
+
/// let client = ${clientName}::with_auth(nats_client, auth);
|
|
626
|
+
/// Ok(())
|
|
627
|
+
/// }
|
|
628
|
+
/// \`\`\`
|
|
629
|
+
pub fn with_auth(client: async_nats::Client, auth: AuthCredentials) -> ClientResult<Self> {
|
|
630
|
+
auth.validate()?;
|
|
631
|
+
Ok(Self {
|
|
632
|
+
client,
|
|
633
|
+
auth: Some(auth),
|
|
634
|
+
})
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
/// Update authentication credentials
|
|
638
|
+
///
|
|
639
|
+
/// This method allows you to update the authentication credentials for an existing client.
|
|
640
|
+
/// All subsequent operations will use the new credentials.
|
|
641
|
+
///
|
|
642
|
+
/// # Arguments
|
|
643
|
+
/// * \`auth\` - New authentication credentials
|
|
644
|
+
///
|
|
645
|
+
/// # Example
|
|
646
|
+
/// \`\`\`ignore
|
|
647
|
+
/// use your_crate::AuthCredentials;
|
|
648
|
+
///
|
|
649
|
+
/// let new_auth = AuthCredentials::jwt("new-jwt-token");
|
|
650
|
+
/// client.update_auth(new_auth)?;
|
|
651
|
+
/// \`\`\`
|
|
652
|
+
pub fn update_auth(&mut self, auth: AuthCredentials) -> ClientResult<()> {
|
|
653
|
+
auth.validate()?;
|
|
654
|
+
self.auth = Some(auth);
|
|
655
|
+
Ok(())
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/// Remove authentication credentials
|
|
659
|
+
///
|
|
660
|
+
/// After calling this method, subsequent operations will not include authentication headers.
|
|
661
|
+
pub fn clear_auth(&mut self) {
|
|
662
|
+
self.auth = None;
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
/// Get the current authentication credentials
|
|
666
|
+
pub fn auth(&self) -> Option<&AuthCredentials> {
|
|
667
|
+
self.auth.as_ref()
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/// Check if authentication credentials are configured
|
|
671
|
+
pub fn has_auth(&self) -> bool {
|
|
672
|
+
self.auth.as_ref().map_or(false, |auth| auth.has_credentials())
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
${generateClientMethods()}
|
|
676
|
+
}
|
|
677
|
+
${generateChannelServices()}
|
|
678
|
+
`}
|
|
679
|
+
</File>
|
|
680
|
+
);
|
|
681
|
+
}
|