@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,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
+ };