@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,199 @@
|
|
|
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="envelope.rs">
|
|
7
|
+
{`//! Message envelope for consistent NATS message format
|
|
8
|
+
|
|
9
|
+
use crate::auth::{AuthCredentials, generate_auth_headers};
|
|
10
|
+
use serde::{Deserialize, Serialize};
|
|
11
|
+
use std::collections::HashMap;
|
|
12
|
+
use uuid::Uuid;
|
|
13
|
+
|
|
14
|
+
/// MessageEnvelope for consistent message format across NATS operations
|
|
15
|
+
/// This matches the format expected by the server implementation
|
|
16
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
17
|
+
pub struct MessageEnvelope {
|
|
18
|
+
/// Unique message identifier
|
|
19
|
+
pub id: String,
|
|
20
|
+
/// Operation name from AsyncAPI spec
|
|
21
|
+
pub operation: String,
|
|
22
|
+
/// Message payload as JSON value
|
|
23
|
+
pub payload: serde_json::Value,
|
|
24
|
+
/// ISO 8601 timestamp when message was created
|
|
25
|
+
pub timestamp: String,
|
|
26
|
+
/// Optional correlation ID for request/reply patterns
|
|
27
|
+
pub correlation_id: Option<String>,
|
|
28
|
+
/// Optional headers for additional metadata
|
|
29
|
+
pub headers: Option<HashMap<String, String>>,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
impl MessageEnvelope {
|
|
33
|
+
/// Create a new message envelope with generated ID and current timestamp
|
|
34
|
+
pub fn new<T: Serialize>(operation: &str, payload: T) -> Result<Self, serde_json::Error> {
|
|
35
|
+
Ok(Self {
|
|
36
|
+
id: Uuid::new_v4().to_string(),
|
|
37
|
+
operation: operation.to_string(),
|
|
38
|
+
payload: serde_json::to_value(payload)?,
|
|
39
|
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
40
|
+
correlation_id: None,
|
|
41
|
+
headers: None,
|
|
42
|
+
})
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/// Create a new message envelope with a specific correlation ID
|
|
46
|
+
pub fn new_with_correlation_id<T: Serialize>(
|
|
47
|
+
operation: &str,
|
|
48
|
+
payload: T,
|
|
49
|
+
correlation_id: String,
|
|
50
|
+
) -> Result<Self, serde_json::Error> {
|
|
51
|
+
Ok(Self {
|
|
52
|
+
id: Uuid::new_v4().to_string(),
|
|
53
|
+
operation: operation.to_string(),
|
|
54
|
+
payload: serde_json::to_value(payload)?,
|
|
55
|
+
timestamp: chrono::Utc::now().to_rfc3339(),
|
|
56
|
+
correlation_id: Some(correlation_id),
|
|
57
|
+
headers: None,
|
|
58
|
+
})
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/// Add headers to the envelope
|
|
62
|
+
pub fn with_headers(mut self, headers: HashMap<String, String>) -> Self {
|
|
63
|
+
self.headers = Some(headers);
|
|
64
|
+
self
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/// Add a single header to the envelope
|
|
68
|
+
pub fn with_header(mut self, key: String, value: String) -> Self {
|
|
69
|
+
if let Some(ref mut headers) = self.headers {
|
|
70
|
+
headers.insert(key, value);
|
|
71
|
+
} else {
|
|
72
|
+
let mut headers = HashMap::new();
|
|
73
|
+
headers.insert(key, value);
|
|
74
|
+
self.headers = Some(headers);
|
|
75
|
+
}
|
|
76
|
+
self
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/// Add authentication headers to the envelope
|
|
80
|
+
pub fn with_auth_headers(mut self, auth: &AuthCredentials) -> Self {
|
|
81
|
+
let auth_headers = generate_auth_headers(auth);
|
|
82
|
+
if !auth_headers.is_empty() {
|
|
83
|
+
if let Some(ref mut headers) = self.headers {
|
|
84
|
+
headers.extend(auth_headers);
|
|
85
|
+
} else {
|
|
86
|
+
self.headers = Some(auth_headers);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
self
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Create a new message envelope with authentication headers
|
|
93
|
+
pub fn new_with_auth<T: Serialize>(
|
|
94
|
+
operation: &str,
|
|
95
|
+
payload: T,
|
|
96
|
+
auth: &AuthCredentials,
|
|
97
|
+
) -> Result<Self, serde_json::Error> {
|
|
98
|
+
let envelope = Self::new(operation, payload)?;
|
|
99
|
+
Ok(envelope.with_auth_headers(auth))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/// Create a new message envelope with correlation ID and authentication headers
|
|
103
|
+
pub fn new_with_correlation_id_and_auth<T: Serialize>(
|
|
104
|
+
operation: &str,
|
|
105
|
+
payload: T,
|
|
106
|
+
correlation_id: String,
|
|
107
|
+
auth: &AuthCredentials,
|
|
108
|
+
) -> Result<Self, serde_json::Error> {
|
|
109
|
+
let envelope = Self::new_with_correlation_id(operation, payload, correlation_id)?;
|
|
110
|
+
Ok(envelope.with_auth_headers(auth))
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Convert the envelope to bytes for NATS transmission
|
|
114
|
+
pub fn to_bytes(&self) -> Result<Vec<u8>, serde_json::Error> {
|
|
115
|
+
serde_json::to_vec(self)
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/// Parse envelope from bytes received from NATS
|
|
119
|
+
pub fn from_bytes(bytes: &[u8]) -> Result<Self, serde_json::Error> {
|
|
120
|
+
serde_json::from_slice(bytes)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Extract the payload as a specific type
|
|
124
|
+
pub fn extract_payload<T: for<'de> Deserialize<'de>>(&self) -> Result<T, serde_json::Error> {
|
|
125
|
+
serde_json::from_value(self.payload.clone())
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/// Get the correlation ID if present
|
|
129
|
+
pub fn correlation_id(&self) -> Option<&str> {
|
|
130
|
+
self.correlation_id.as_deref()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
#[cfg(test)]
|
|
135
|
+
mod tests {
|
|
136
|
+
use super::*;
|
|
137
|
+
use serde::{Deserialize, Serialize};
|
|
138
|
+
|
|
139
|
+
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
140
|
+
struct TestPayload {
|
|
141
|
+
message: String,
|
|
142
|
+
count: u32,
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
#[test]
|
|
146
|
+
fn test_envelope_creation() {
|
|
147
|
+
let payload = TestPayload {
|
|
148
|
+
message: "test".to_string(),
|
|
149
|
+
count: 42,
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
153
|
+
|
|
154
|
+
assert_eq!(envelope.operation, "test_operation");
|
|
155
|
+
assert!(!envelope.id.is_empty());
|
|
156
|
+
assert!(!envelope.timestamp.is_empty());
|
|
157
|
+
assert_eq!(envelope.correlation_id, None);
|
|
158
|
+
|
|
159
|
+
let extracted: TestPayload = envelope.extract_payload().unwrap();
|
|
160
|
+
assert_eq!(extracted, payload);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
#[test]
|
|
164
|
+
fn test_envelope_with_correlation_id() {
|
|
165
|
+
let payload = TestPayload {
|
|
166
|
+
message: "test".to_string(),
|
|
167
|
+
count: 42,
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
let correlation_id = "test-correlation-id".to_string();
|
|
171
|
+
let envelope = MessageEnvelope::new_with_correlation_id(
|
|
172
|
+
"test_operation",
|
|
173
|
+
&payload,
|
|
174
|
+
correlation_id.clone(),
|
|
175
|
+
).unwrap();
|
|
176
|
+
|
|
177
|
+
assert_eq!(envelope.correlation_id(), Some(correlation_id.as_str()));
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
#[test]
|
|
181
|
+
fn test_envelope_serialization() {
|
|
182
|
+
let payload = TestPayload {
|
|
183
|
+
message: "test".to_string(),
|
|
184
|
+
count: 42,
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
188
|
+
let bytes = envelope.to_bytes().unwrap();
|
|
189
|
+
let deserialized = MessageEnvelope::from_bytes(&bytes).unwrap();
|
|
190
|
+
|
|
191
|
+
assert_eq!(envelope.id, deserialized.id);
|
|
192
|
+
assert_eq!(envelope.operation, deserialized.operation);
|
|
193
|
+
assert_eq!(envelope.timestamp, deserialized.timestamp);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
`}
|
|
197
|
+
</File>
|
|
198
|
+
);
|
|
199
|
+
};
|
|
@@ -0,0 +1,49 @@
|
|
|
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.rs">
|
|
7
|
+
{`//! Error types for the NATS client
|
|
8
|
+
|
|
9
|
+
use crate::auth::AuthError;
|
|
10
|
+
use thiserror::Error;
|
|
11
|
+
|
|
12
|
+
/// Errors that can occur when using the NATS client
|
|
13
|
+
#[derive(Debug, Error)]
|
|
14
|
+
pub enum ClientError {
|
|
15
|
+
/// NATS operation failed
|
|
16
|
+
#[error("NATS operation failed: {0}")]
|
|
17
|
+
Nats(Box<dyn std::error::Error + Send + Sync>),
|
|
18
|
+
|
|
19
|
+
/// Serialization or deserialization failed
|
|
20
|
+
#[error("Serialization failed: {0}")]
|
|
21
|
+
Serialization(#[from] serde_json::Error),
|
|
22
|
+
|
|
23
|
+
/// Invalid message envelope format
|
|
24
|
+
#[error("Invalid message envelope: {0}")]
|
|
25
|
+
InvalidEnvelope(String),
|
|
26
|
+
|
|
27
|
+
/// Operation timeout
|
|
28
|
+
#[error("Operation timed out")]
|
|
29
|
+
Timeout,
|
|
30
|
+
|
|
31
|
+
/// No response received for request
|
|
32
|
+
#[error("No response received")]
|
|
33
|
+
NoResponse,
|
|
34
|
+
|
|
35
|
+
/// Authentication error
|
|
36
|
+
#[error("Authentication error: {0}")]
|
|
37
|
+
Auth(#[from] AuthError),
|
|
38
|
+
|
|
39
|
+
/// Unauthorized access
|
|
40
|
+
#[error("Unauthorized: {0}")]
|
|
41
|
+
Unauthorized(String),
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Result type alias for client operations
|
|
45
|
+
pub type ClientResult<T> = Result<T, ClientError>;
|
|
46
|
+
`}
|
|
47
|
+
</File>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
|
|
4
|
+
module.exports = function ({ asyncapi, params }) {
|
|
5
|
+
// Helper function to convert title to PascalCase
|
|
6
|
+
function toPascalCase(str) {
|
|
7
|
+
return str.replace(/[^a-zA-Z0-9]/g, ' ')
|
|
8
|
+
.split(' ')
|
|
9
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
|
10
|
+
.join('');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const title = asyncapi.info().title();
|
|
14
|
+
const description = asyncapi.info().description();
|
|
15
|
+
const version = asyncapi.info().version();
|
|
16
|
+
|
|
17
|
+
const clientName = (params.clientName && !params.clientName.includes('{{'))
|
|
18
|
+
? params.clientName
|
|
19
|
+
: `${toPascalCase(title)}Client`;
|
|
20
|
+
|
|
21
|
+
// Helper function to format description for Rust doc comments
|
|
22
|
+
function formatDescription(desc) {
|
|
23
|
+
if (!desc) return 'Generated Rust AsyncAPI NATS client';
|
|
24
|
+
return desc.split('\n').map(line => line.trim()).filter(line => line).join('\n//! ');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<File name="lib.rs">
|
|
29
|
+
{`//! ${title}
|
|
30
|
+
//!
|
|
31
|
+
//! ${formatDescription(description)}
|
|
32
|
+
//!
|
|
33
|
+
//! This crate provides a type-safe NATS client generated from an AsyncAPI specification.
|
|
34
|
+
//! It supports both request/reply and pub/sub messaging patterns using the NATS protocol.
|
|
35
|
+
//!
|
|
36
|
+
//! # Features
|
|
37
|
+
//!
|
|
38
|
+
//! - **Type Safety**: All message types are generated from AsyncAPI schemas
|
|
39
|
+
//! - **NATS Integration**: Uses the official async-nats client library
|
|
40
|
+
//! - **Request/Reply**: Supports NATS request/reply patterns for synchronous operations
|
|
41
|
+
//! - **Pub/Sub**: Supports NATS publish/subscribe patterns for asynchronous messaging
|
|
42
|
+
//! - **Message Envelope**: Consistent message format with metadata and correlation IDs
|
|
43
|
+
//! - **Error Handling**: Comprehensive error types for different failure scenarios
|
|
44
|
+
//!
|
|
45
|
+
//! # Quick Start
|
|
46
|
+
//!
|
|
47
|
+
//! \`\`\`ignore
|
|
48
|
+
//! use async_nats;
|
|
49
|
+
//! use ${params.packageName?.replace(/-/g, '_') || 'your_crate'}::${clientName};
|
|
50
|
+
//!
|
|
51
|
+
//! #[tokio::main]
|
|
52
|
+
//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
53
|
+
//! // Connect to NATS server
|
|
54
|
+
//! let nats_client = async_nats::connect("nats://localhost:4222").await?;
|
|
55
|
+
//!
|
|
56
|
+
//! // Create the generated client
|
|
57
|
+
//! let client = ${clientName}::with(nats_client);
|
|
58
|
+
//!
|
|
59
|
+
//! // Use the client for operations...
|
|
60
|
+
//!
|
|
61
|
+
//! Ok(())
|
|
62
|
+
//! }
|
|
63
|
+
//! \`\`\`
|
|
64
|
+
//!
|
|
65
|
+
//! # Authentication
|
|
66
|
+
//!
|
|
67
|
+
//! The client accepts any configured \`async-nats::Client\`, allowing you to handle
|
|
68
|
+
//! authentication at the NATS level:
|
|
69
|
+
//!
|
|
70
|
+
//! \`\`\`ignore
|
|
71
|
+
//! // JWT authentication
|
|
72
|
+
//! let nats_client = async_nats::ConnectOptions::new()
|
|
73
|
+
//! .credentials_file("./service.creds").await?
|
|
74
|
+
//! .connect("nats://server:4222").await?;
|
|
75
|
+
//!
|
|
76
|
+
//! let client = ${clientName}::with(nats_client);
|
|
77
|
+
//! \`\`\`
|
|
78
|
+
//!
|
|
79
|
+
//! # Generated from AsyncAPI
|
|
80
|
+
//!
|
|
81
|
+
//! - **AsyncAPI Version**: ${asyncapi.version()}
|
|
82
|
+
//! - **Generated**: ${new Date().toISOString()}
|
|
83
|
+
//! - **Title**: ${title}
|
|
84
|
+
//! - **Version**: ${version}
|
|
85
|
+
|
|
86
|
+
pub mod auth;
|
|
87
|
+
pub mod client;
|
|
88
|
+
pub mod envelope;
|
|
89
|
+
pub mod errors;
|
|
90
|
+
pub mod models;
|
|
91
|
+
|
|
92
|
+
// Re-export main types for convenience
|
|
93
|
+
pub use auth::{AuthCredentials, generate_auth_headers};
|
|
94
|
+
pub use client::${clientName};
|
|
95
|
+
pub use envelope::MessageEnvelope;
|
|
96
|
+
pub use errors::{ClientError, ClientResult};
|
|
97
|
+
|
|
98
|
+
// Re-export all models
|
|
99
|
+
pub use models::*;
|
|
100
|
+
|
|
101
|
+
#[cfg(test)]
|
|
102
|
+
mod tests {
|
|
103
|
+
#[test]
|
|
104
|
+
fn test_client_creation() {
|
|
105
|
+
// This test requires a NATS client, so we'll just test compilation
|
|
106
|
+
// In real usage, you would create an async-nats::Client first
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
`}
|
|
110
|
+
</File>
|
|
111
|
+
);
|
|
112
|
+
};
|