@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,244 @@
1
+ const { File } = require('@asyncapi/generator-react-sdk');
2
+ const React = require('react');
3
+
4
+ module.exports = function ({ asyncapi, params }) {
5
+ // Extract info from AsyncAPI spec
6
+ let title, version, description;
7
+ try {
8
+ const info = asyncapi.info();
9
+ title = info.title();
10
+ version = info.version();
11
+ description = info.description();
12
+ } catch (error) {
13
+ title = 'UnknownAPI';
14
+ version = '1.0.0';
15
+ description = 'Generated NATS client';
16
+ }
17
+
18
+ // Helper function to check if a parameter contains unresolved template variables
19
+ function isTemplateVariable(value) {
20
+ return typeof value === 'string' && value.includes('{{') && value.includes('}}');
21
+ }
22
+
23
+ // Helper function to convert title to kebab-case
24
+ function toKebabCase(str) {
25
+ return str.toLowerCase().replace(/[^a-z0-9]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, '');
26
+ }
27
+
28
+ // Helper function to convert title to PascalCase for struct names
29
+ function toPascalCase(str) {
30
+ return str.replace(/[^a-zA-Z0-9]/g, ' ')
31
+ .split(' ')
32
+ .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
33
+ .join('');
34
+ }
35
+
36
+ // Helper function to convert to snake_case for Rust identifiers
37
+ function toSnakeCase(str) {
38
+ return str.toLowerCase().replace(/[^a-z0-9]/g, '_').replace(/_+/g, '_').replace(/^_|_$/g, '');
39
+ }
40
+
41
+ // Resolve parameters, falling back to extracted values if parameters contain template variables
42
+ const clientName = (params.clientName && !isTemplateVariable(params.clientName))
43
+ ? params.clientName
44
+ : `${toPascalCase(title)}Client`;
45
+
46
+ const packageName = (params.packageName && !isTemplateVariable(params.packageName))
47
+ ? params.packageName
48
+ : `${toKebabCase(title)}-client`;
49
+
50
+ const packageVersion = (params.packageVersion && !isTemplateVariable(params.packageVersion))
51
+ ? params.packageVersion
52
+ : version;
53
+
54
+ const license = (params.license && !isTemplateVariable(params.license))
55
+ ? params.license
56
+ : 'Apache-2.0';
57
+
58
+ // Import the source file generators
59
+ const LibRs = require('./src/lib.rs.js');
60
+ const ClientRs = require('./src/client.rs.js');
61
+ const ErrorsRs = require('./src/errors.rs.js');
62
+ const EnvelopeRs = require('./src/envelope.rs.js');
63
+ const ModelsRs = require('./src/models.rs.js');
64
+ const AuthRs = require('./src/auth.rs.js');
65
+ const CargoToml = require('./Cargo.toml.js');
66
+
67
+ // Generate all files from the main index.js
68
+ return [
69
+ // Use the separate Cargo.toml template
70
+ React.createElement(CargoToml, { asyncapi, params }),
71
+
72
+ // Generate Rust source files
73
+ React.createElement(LibRs, { asyncapi, params }),
74
+ React.createElement(ClientRs, { asyncapi, params }),
75
+ React.createElement(ErrorsRs, { asyncapi, params }),
76
+ React.createElement(EnvelopeRs, { asyncapi, params }),
77
+ React.createElement(ModelsRs, { asyncapi, params }),
78
+ React.createElement(AuthRs, { asyncapi, params }),
79
+
80
+ React.createElement(File, { name: 'README.md' },
81
+ `# ${title}
82
+
83
+ ${description || 'Generated Rust AsyncAPI NATS client'}
84
+
85
+ ## Overview
86
+
87
+ This Rust client provides type-safe access to your AsyncAPI service using NATS messaging. Generated from your AsyncAPI specification, it offers seamless integration with NATS request/reply and pub/sub patterns.
88
+
89
+ ## Technical Requirements
90
+
91
+ - Rust 1.70+
92
+ - NATS server
93
+
94
+ ## Installation
95
+
96
+ Add this to your \`Cargo.toml\`:
97
+
98
+ \`\`\`toml
99
+ [dependencies]
100
+ ${packageName} = "${packageVersion}"
101
+ async-nats = "0.33"
102
+ tokio = { version = "1.0", features = ["full"] }
103
+ \`\`\`
104
+
105
+ ## Quick Start
106
+
107
+ ### Basic Usage
108
+
109
+ \`\`\`rust
110
+ use async_nats;
111
+ use ${toSnakeCase(packageName)}::${clientName};
112
+
113
+ #[tokio::main]
114
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
115
+ // Set up your NATS client with desired configuration
116
+ let nats_client = async_nats::connect("nats://localhost:4222").await?;
117
+
118
+ // Create the service client
119
+ let client = ${clientName}::with(nats_client);
120
+
121
+ // Use the generated methods
122
+ // (see generated documentation for specific operations)
123
+
124
+ Ok(())
125
+ }
126
+ \`\`\`
127
+
128
+ ### With Authentication Headers
129
+
130
+ \`\`\`rust
131
+ use async_nats;
132
+ use ${toSnakeCase(packageName)}::{${clientName}, AuthCredentials};
133
+
134
+ #[tokio::main]
135
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
136
+ let nats_client = async_nats::connect("nats://localhost:4222").await?;
137
+
138
+ // Create client with JWT authentication
139
+ let auth = AuthCredentials::jwt("your-jwt-token");
140
+ let client = ${clientName}::with_auth(nats_client, auth)?;
141
+
142
+ // Or with Basic authentication
143
+ // let auth = AuthCredentials::basic("username", "password");
144
+ // let client = ${clientName}::with_auth(nats_client, auth)?;
145
+
146
+ // Or with API Key authentication
147
+ // let auth = AuthCredentials::apikey_header("X-API-Key", "your-api-key");
148
+ // let client = ${clientName}::with_auth(nats_client, auth)?;
149
+
150
+ // All operations will now include authentication headers
151
+ // let result = client.some_operation(payload).await?;
152
+
153
+ Ok(())
154
+ }
155
+ \`\`\`
156
+
157
+ ### With NATS-level Authentication
158
+
159
+ \`\`\`rust
160
+ use async_nats;
161
+ use ${toSnakeCase(packageName)}::${clientName};
162
+
163
+ #[tokio::main]
164
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
165
+ // Set up NATS client with JWT credentials
166
+ let nats_client = async_nats::ConnectOptions::new()
167
+ .credentials_file("./service.creds").await?
168
+ .name("${toKebabCase(title)}-client")
169
+ .connect("nats://server:4222").await?;
170
+
171
+ let client = ${clientName}::with(nats_client);
172
+
173
+ // Use the client...
174
+
175
+ Ok(())
176
+ }
177
+ \`\`\`
178
+
179
+ ### Shared Client Usage
180
+
181
+ \`\`\`rust
182
+ use async_nats;
183
+ use ${toSnakeCase(packageName)}::${clientName};
184
+
185
+ #[tokio::main]
186
+ async fn main() -> Result<(), Box<dyn std::error::Error>> {
187
+ // Single NATS client can be shared across multiple service clients
188
+ let nats_client = async_nats::connect("nats://localhost:4222").await?;
189
+
190
+ let client = ${clientName}::with(nats_client.clone());
191
+ // You can create other service clients with the same nats_client
192
+
193
+ Ok(())
194
+ }
195
+ \`\`\`
196
+
197
+ ## Configuration
198
+
199
+ The client accepts any \`async-nats::Client\`, giving you full control over:
200
+
201
+ - **Authentication**: JWT, NKey, username/password, token
202
+ - **TLS**: Custom certificates and encryption
203
+ - **Connection**: Timeouts, retry logic, clustering
204
+ - **Monitoring**: Connection events and health checks
205
+
206
+ See the [async-nats documentation](https://docs.rs/async-nats/) for complete configuration options.
207
+
208
+ ## Error Handling
209
+
210
+ The client provides specific error types for different scenarios:
211
+
212
+ \`\`\`rust
213
+ use ${toSnakeCase(packageName)}::{${clientName}, ClientError};
214
+
215
+ match client.some_operation(payload).await {
216
+ Ok(result) => println!("Success: {:?}", result),
217
+ Err(ClientError::Nats(e)) => eprintln!("NATS error: {}", e),
218
+ Err(ClientError::Serialization(e)) => eprintln!("Serialization error: {}", e),
219
+ Err(ClientError::InvalidEnvelope(e)) => eprintln!("Invalid message: {}", e),
220
+ }
221
+ \`\`\`
222
+
223
+ ## Generated from AsyncAPI
224
+
225
+ - **AsyncAPI Version**: ${asyncapi.version()}
226
+ - **Generated**: ${new Date().toISOString()}
227
+ - **Title**: ${title}
228
+ - **Version**: ${packageVersion}
229
+
230
+ ## Contributing
231
+
232
+ 1. Fork the repository
233
+ 2. Create a feature branch
234
+ 3. Make your changes and add tests
235
+ 4. Run the test suite: \`cargo test\`
236
+ 5. Submit a pull request
237
+
238
+ ## License
239
+
240
+ ${license}
241
+ `
242
+ )
243
+ ];
244
+ };
@@ -0,0 +1,48 @@
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
+ }
@@ -0,0 +1,221 @@
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="auth.rs">
7
+ {`//! Authentication support for the NATS client
8
+
9
+ use base64::Engine;
10
+ use serde::{Deserialize, Serialize};
11
+ use std::collections::HashMap;
12
+ use thiserror::Error;
13
+
14
+ /// Authentication credentials for different auth types
15
+ #[derive(Debug, Clone, Serialize, Deserialize)]
16
+ pub struct AuthCredentials {
17
+ /// JWT Bearer token
18
+ pub jwt: Option<String>,
19
+ /// Basic authentication credentials
20
+ pub basic: Option<BasicAuth>,
21
+ /// API Key authentication
22
+ pub apikey: Option<ApiKeyAuth>,
23
+ }
24
+
25
+ /// Basic authentication credentials
26
+ #[derive(Debug, Clone, Serialize, Deserialize)]
27
+ pub struct BasicAuth {
28
+ pub username: String,
29
+ pub password: String,
30
+ }
31
+
32
+ /// API Key authentication configuration
33
+ #[derive(Debug, Clone, Serialize, Deserialize)]
34
+ pub struct ApiKeyAuth {
35
+ pub key: String,
36
+ pub name: String,
37
+ }
38
+
39
+ /// Auth-related error types
40
+ #[derive(Debug, Error)]
41
+ pub enum AuthError {
42
+ #[error("JWT token must be a non-empty string")]
43
+ InvalidJwt,
44
+
45
+ #[error("Basic auth requires both username and password")]
46
+ InvalidBasicAuth,
47
+
48
+ #[error("API key auth requires both key and name")]
49
+ InvalidApiKey,
50
+
51
+ #[error("Token has expired")]
52
+ TokenExpired,
53
+
54
+ #[error("Unauthorized access")]
55
+ Unauthorized,
56
+
57
+ #[error("Authentication failed: {0}")]
58
+ AuthenticationFailed(String),
59
+ }
60
+
61
+ impl AuthCredentials {
62
+ /// Create new JWT credentials
63
+ pub fn jwt(token: impl Into<String>) -> Self {
64
+ Self {
65
+ jwt: Some(token.into()),
66
+ basic: None,
67
+ apikey: None,
68
+ }
69
+ }
70
+
71
+ /// Create new Basic auth credentials
72
+ pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
73
+ Self {
74
+ jwt: None,
75
+ basic: Some(BasicAuth {
76
+ username: username.into(),
77
+ password: password.into(),
78
+ }),
79
+ apikey: None,
80
+ }
81
+ }
82
+
83
+ /// Create new API key credentials for header
84
+ pub fn apikey_header(name: impl Into<String>, key: impl Into<String>) -> Self {
85
+ Self {
86
+ jwt: None,
87
+ basic: None,
88
+ apikey: Some(ApiKeyAuth {
89
+ key: key.into(),
90
+ name: name.into(),
91
+ }),
92
+ }
93
+ }
94
+
95
+ /// Check if any credentials are provided
96
+ pub fn has_credentials(&self) -> bool {
97
+ self.jwt.is_some() || self.basic.is_some() || self.apikey.is_some()
98
+ }
99
+
100
+ /// Get the auth type as a string
101
+ pub fn auth_type(&self) -> Option<&'static str> {
102
+ if self.jwt.is_some() {
103
+ Some("jwt")
104
+ } else if self.basic.is_some() {
105
+ Some("basic")
106
+ } else if self.apikey.is_some() {
107
+ Some("apikey")
108
+ } else {
109
+ None
110
+ }
111
+ }
112
+
113
+ /// Validate the credentials
114
+ pub fn validate(&self) -> Result<(), AuthError> {
115
+ if let Some(ref jwt) = self.jwt {
116
+ if jwt.trim().is_empty() {
117
+ return Err(AuthError::InvalidJwt);
118
+ }
119
+ }
120
+
121
+ if let Some(ref basic) = self.basic {
122
+ if basic.username.is_empty() || basic.password.is_empty() {
123
+ return Err(AuthError::InvalidBasicAuth);
124
+ }
125
+ }
126
+
127
+ if let Some(ref apikey) = self.apikey {
128
+ if apikey.key.is_empty() || apikey.name.is_empty() {
129
+ return Err(AuthError::InvalidApiKey);
130
+ }
131
+ }
132
+
133
+ Ok(())
134
+ }
135
+ }
136
+
137
+ /// Generate authentication headers based on credentials
138
+ pub fn generate_auth_headers(auth: &AuthCredentials) -> HashMap<String, String> {
139
+ let mut headers = HashMap::new();
140
+
141
+ if let Some(ref jwt) = auth.jwt {
142
+ headers.insert("Authorization".to_string(), format!("Bearer {}", jwt));
143
+ } else if let Some(ref basic) = auth.basic {
144
+ let credentials = base64::engine::general_purpose::STANDARD.encode(format!("{}:{}", basic.username, basic.password));
145
+ headers.insert("Authorization".to_string(), format!("Basic {}", credentials));
146
+ } else if let Some(ref apikey) = auth.apikey {
147
+ headers.insert(apikey.name.clone(), apikey.key.clone());
148
+ }
149
+
150
+ headers
151
+ }
152
+
153
+ #[cfg(test)]
154
+ mod tests {
155
+ use super::*;
156
+
157
+ #[test]
158
+ fn test_jwt_credentials() {
159
+ let auth = AuthCredentials::jwt("test-token");
160
+ assert!(auth.has_credentials());
161
+ assert_eq!(auth.auth_type(), Some("jwt"));
162
+ assert!(auth.validate().is_ok());
163
+
164
+ let headers = generate_auth_headers(&auth);
165
+ assert_eq!(headers.get("Authorization"), Some(&"Bearer test-token".to_string()));
166
+ }
167
+
168
+ #[test]
169
+ fn test_basic_credentials() {
170
+ let auth = AuthCredentials::basic("user", "pass");
171
+ assert!(auth.has_credentials());
172
+ assert_eq!(auth.auth_type(), Some("basic"));
173
+ assert!(auth.validate().is_ok());
174
+
175
+ let headers = generate_auth_headers(&auth);
176
+ let expected = format!("Basic {}", base64::engine::general_purpose::STANDARD.encode("user:pass"));
177
+ assert_eq!(headers.get("Authorization"), Some(&expected));
178
+ }
179
+
180
+ #[test]
181
+ fn test_apikey_header_credentials() {
182
+ let auth = AuthCredentials::apikey_header("X-API-Key", "secret");
183
+ assert!(auth.has_credentials());
184
+ assert_eq!(auth.auth_type(), Some("apikey"));
185
+ assert!(auth.validate().is_ok());
186
+
187
+ let headers = generate_auth_headers(&auth);
188
+ assert_eq!(headers.get("X-API-Key"), Some(&"secret".to_string()));
189
+ }
190
+
191
+ #[test]
192
+ fn test_empty_credentials() {
193
+ let auth = AuthCredentials {
194
+ jwt: None,
195
+ basic: None,
196
+ apikey: None,
197
+ };
198
+ assert!(!auth.has_credentials());
199
+ assert_eq!(auth.auth_type(), None);
200
+ assert!(auth.validate().is_ok());
201
+ }
202
+
203
+ #[test]
204
+ fn test_invalid_jwt() {
205
+ let auth = AuthCredentials::jwt("");
206
+ assert!(auth.validate().is_err());
207
+ }
208
+
209
+ #[test]
210
+ fn test_invalid_basic() {
211
+ let auth = AuthCredentials::basic("", "pass");
212
+ assert!(auth.validate().is_err());
213
+
214
+ let auth = AuthCredentials::basic("user", "");
215
+ assert!(auth.validate().is_err());
216
+ }
217
+ }
218
+ `}
219
+ </File>
220
+ );
221
+ };