@ioka-technologies/asyncapi-rust-client-template 0.0.30 → 0.0.32
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/dist/common/index.js +329 -150
- package/package.json +1 -1
- package/template/src/client.rs.js +12 -0
- package/template/src/envelope.rs.js +8 -176
- package/template/src/envelope.rs.js.dev +31 -0
- package/template/src/errors.rs.js +55 -0
package/dist/common/index.js
CHANGED
|
@@ -2305,6 +2305,14 @@ function generateRustModels(asyncapi, options = {}) {
|
|
|
2305
2305
|
return 'u32';
|
|
2306
2306
|
case 'uint64':
|
|
2307
2307
|
return 'u64';
|
|
2308
|
+
case 'uint16':
|
|
2309
|
+
return 'u16';
|
|
2310
|
+
case 'uint8':
|
|
2311
|
+
return 'u8';
|
|
2312
|
+
case 'int16':
|
|
2313
|
+
return 'i16';
|
|
2314
|
+
case 'int8':
|
|
2315
|
+
return 'i8';
|
|
2308
2316
|
default:
|
|
2309
2317
|
// Default to i32 for unspecified format (maintains backward compatibility)
|
|
2310
2318
|
return 'i32';
|
|
@@ -2718,6 +2726,287 @@ function generateMessageEnvelope() {
|
|
|
2718
2726
|
return `use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
|
2719
2727
|
use std::collections::HashMap;
|
|
2720
2728
|
use uuid::Uuid;
|
|
2729
|
+
use chrono::{DateTime, Utc};
|
|
2730
|
+
|
|
2731
|
+
/// Correlation ID for tracing errors across operations
|
|
2732
|
+
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
|
2733
|
+
pub struct CorrelationId(pub Uuid);
|
|
2734
|
+
|
|
2735
|
+
impl CorrelationId {
|
|
2736
|
+
pub fn new() -> Self {
|
|
2737
|
+
Self(Uuid::new_v4())
|
|
2738
|
+
}
|
|
2739
|
+
}
|
|
2740
|
+
|
|
2741
|
+
impl std::fmt::Display for CorrelationId {
|
|
2742
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
2743
|
+
write!(f, "{}", self.0)
|
|
2744
|
+
}
|
|
2745
|
+
}
|
|
2746
|
+
|
|
2747
|
+
impl Default for CorrelationId {
|
|
2748
|
+
fn default() -> Self {
|
|
2749
|
+
Self::new()
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
|
|
2753
|
+
/// Error severity levels for categorization and alerting
|
|
2754
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
2755
|
+
pub enum ErrorSeverity {
|
|
2756
|
+
/// Low severity - informational, no action required
|
|
2757
|
+
Low,
|
|
2758
|
+
/// Medium severity - warning, monitoring required
|
|
2759
|
+
Medium,
|
|
2760
|
+
/// High severity - error, immediate attention needed
|
|
2761
|
+
High,
|
|
2762
|
+
/// Critical severity - system failure, urgent action required
|
|
2763
|
+
Critical,
|
|
2764
|
+
}
|
|
2765
|
+
|
|
2766
|
+
impl std::fmt::Display for ErrorSeverity {
|
|
2767
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
2768
|
+
match self {
|
|
2769
|
+
ErrorSeverity::Low => write!(f, "LOW"),
|
|
2770
|
+
ErrorSeverity::Medium => write!(f, "MEDIUM"),
|
|
2771
|
+
ErrorSeverity::High => write!(f, "HIGH"),
|
|
2772
|
+
ErrorSeverity::Critical => write!(f, "CRITICAL"),
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
}
|
|
2776
|
+
|
|
2777
|
+
/// Error category for classification and handling
|
|
2778
|
+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
2779
|
+
pub enum ErrorCategory {
|
|
2780
|
+
/// Configuration-related errors
|
|
2781
|
+
Configuration,
|
|
2782
|
+
/// Network and protocol errors
|
|
2783
|
+
Network,
|
|
2784
|
+
/// Message validation errors
|
|
2785
|
+
Validation,
|
|
2786
|
+
/// Business logic errors
|
|
2787
|
+
BusinessLogic,
|
|
2788
|
+
/// System resource errors
|
|
2789
|
+
Resource,
|
|
2790
|
+
/// Security-related errors
|
|
2791
|
+
Security,
|
|
2792
|
+
/// Serialization/deserialization errors
|
|
2793
|
+
Serialization,
|
|
2794
|
+
/// Routing errors
|
|
2795
|
+
Routing,
|
|
2796
|
+
/// Authorization errors
|
|
2797
|
+
Authorization,
|
|
2798
|
+
/// Unknown or unclassified errors
|
|
2799
|
+
Unknown,
|
|
2800
|
+
}
|
|
2801
|
+
|
|
2802
|
+
impl std::fmt::Display for ErrorCategory {
|
|
2803
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
2804
|
+
match self {
|
|
2805
|
+
ErrorCategory::Configuration => write!(f, "CONFIGURATION"),
|
|
2806
|
+
ErrorCategory::Network => write!(f, "NETWORK"),
|
|
2807
|
+
ErrorCategory::Validation => write!(f, "VALIDATION"),
|
|
2808
|
+
ErrorCategory::BusinessLogic => write!(f, "BUSINESS_LOGIC"),
|
|
2809
|
+
ErrorCategory::Resource => write!(f, "RESOURCE"),
|
|
2810
|
+
ErrorCategory::Security => write!(f, "SECURITY"),
|
|
2811
|
+
ErrorCategory::Serialization => write!(f, "SERIALIZATION"),
|
|
2812
|
+
ErrorCategory::Routing => write!(f, "ROUTING"),
|
|
2813
|
+
ErrorCategory::Authorization => write!(f, "AUTHORIZATION"),
|
|
2814
|
+
ErrorCategory::Unknown => write!(f, "UNKNOWN"),
|
|
2815
|
+
}
|
|
2816
|
+
}
|
|
2817
|
+
}
|
|
2818
|
+
|
|
2819
|
+
/// Error metadata for enhanced context and monitoring
|
|
2820
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
2821
|
+
pub struct ErrorMetadata {
|
|
2822
|
+
pub correlation_id: CorrelationId,
|
|
2823
|
+
pub severity: ErrorSeverity,
|
|
2824
|
+
pub category: ErrorCategory,
|
|
2825
|
+
pub timestamp: DateTime<Utc>,
|
|
2826
|
+
pub retryable: bool,
|
|
2827
|
+
pub kind: u32,
|
|
2828
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
2829
|
+
pub source_location: Option<String>,
|
|
2830
|
+
#[serde(skip_serializing_if = "HashMap::is_empty", default)]
|
|
2831
|
+
pub additional_context: HashMap<String, String>,
|
|
2832
|
+
}
|
|
2833
|
+
|
|
2834
|
+
impl ErrorMetadata {
|
|
2835
|
+
pub fn new(severity: ErrorSeverity, category: ErrorCategory, retryable: bool) -> Self {
|
|
2836
|
+
Self {
|
|
2837
|
+
correlation_id: CorrelationId::new(),
|
|
2838
|
+
severity,
|
|
2839
|
+
category,
|
|
2840
|
+
timestamp: Utc::now(),
|
|
2841
|
+
retryable,
|
|
2842
|
+
kind: 0,
|
|
2843
|
+
source_location: None,
|
|
2844
|
+
additional_context: HashMap::new(),
|
|
2845
|
+
}
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2848
|
+
pub fn with_kind(mut self, kind: u32) -> Self {
|
|
2849
|
+
self.kind = kind;
|
|
2850
|
+
self
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
|
|
2854
|
+
/// Serializable AsyncAPI error for wire transmission
|
|
2855
|
+
///
|
|
2856
|
+
/// This error type can be sent between server and client while preserving
|
|
2857
|
+
/// all the rich error information needed for proper error handling.
|
|
2858
|
+
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
|
2859
|
+
#[serde(tag = "error_type", content = "details")]
|
|
2860
|
+
pub enum AsyncApiError {
|
|
2861
|
+
#[serde(rename = "configuration")]
|
|
2862
|
+
Configuration {
|
|
2863
|
+
message: String,
|
|
2864
|
+
metadata: ErrorMetadata,
|
|
2865
|
+
},
|
|
2866
|
+
|
|
2867
|
+
#[serde(rename = "protocol")]
|
|
2868
|
+
Protocol {
|
|
2869
|
+
message: String,
|
|
2870
|
+
protocol: String,
|
|
2871
|
+
metadata: ErrorMetadata,
|
|
2872
|
+
},
|
|
2873
|
+
|
|
2874
|
+
#[serde(rename = "validation")]
|
|
2875
|
+
Validation {
|
|
2876
|
+
message: String,
|
|
2877
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
2878
|
+
field: Option<String>,
|
|
2879
|
+
metadata: ErrorMetadata,
|
|
2880
|
+
},
|
|
2881
|
+
|
|
2882
|
+
#[serde(rename = "handler")]
|
|
2883
|
+
Handler {
|
|
2884
|
+
message: String,
|
|
2885
|
+
handler_name: String,
|
|
2886
|
+
metadata: ErrorMetadata,
|
|
2887
|
+
},
|
|
2888
|
+
|
|
2889
|
+
#[serde(rename = "middleware")]
|
|
2890
|
+
Middleware {
|
|
2891
|
+
message: String,
|
|
2892
|
+
middleware_name: String,
|
|
2893
|
+
metadata: ErrorMetadata,
|
|
2894
|
+
},
|
|
2895
|
+
|
|
2896
|
+
#[serde(rename = "recovery")]
|
|
2897
|
+
Recovery {
|
|
2898
|
+
message: String,
|
|
2899
|
+
attempts: u32,
|
|
2900
|
+
metadata: ErrorMetadata,
|
|
2901
|
+
},
|
|
2902
|
+
|
|
2903
|
+
#[serde(rename = "resource")]
|
|
2904
|
+
Resource {
|
|
2905
|
+
message: String,
|
|
2906
|
+
resource_type: String,
|
|
2907
|
+
metadata: ErrorMetadata,
|
|
2908
|
+
},
|
|
2909
|
+
|
|
2910
|
+
#[serde(rename = "security")]
|
|
2911
|
+
Security {
|
|
2912
|
+
message: String,
|
|
2913
|
+
metadata: ErrorMetadata,
|
|
2914
|
+
},
|
|
2915
|
+
|
|
2916
|
+
#[serde(rename = "authentication")]
|
|
2917
|
+
Authentication {
|
|
2918
|
+
message: String,
|
|
2919
|
+
auth_method: String,
|
|
2920
|
+
metadata: ErrorMetadata,
|
|
2921
|
+
},
|
|
2922
|
+
|
|
2923
|
+
#[serde(rename = "authorization")]
|
|
2924
|
+
Authorization {
|
|
2925
|
+
message: String,
|
|
2926
|
+
required_permissions: Vec<String>,
|
|
2927
|
+
metadata: ErrorMetadata,
|
|
2928
|
+
},
|
|
2929
|
+
|
|
2930
|
+
#[serde(rename = "rate_limit")]
|
|
2931
|
+
RateLimit {
|
|
2932
|
+
message: String,
|
|
2933
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
2934
|
+
retry_after_secs: Option<u64>,
|
|
2935
|
+
},
|
|
2936
|
+
|
|
2937
|
+
#[serde(rename = "context")]
|
|
2938
|
+
Context {
|
|
2939
|
+
message: String,
|
|
2940
|
+
context_key: String,
|
|
2941
|
+
metadata: ErrorMetadata,
|
|
2942
|
+
},
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2945
|
+
impl AsyncApiError {
|
|
2946
|
+
/// Get error message
|
|
2947
|
+
pub fn message(&self) -> &str {
|
|
2948
|
+
match self {
|
|
2949
|
+
AsyncApiError::Configuration { message, .. } => message,
|
|
2950
|
+
AsyncApiError::Protocol { message, .. } => message,
|
|
2951
|
+
AsyncApiError::Validation { message, .. } => message,
|
|
2952
|
+
AsyncApiError::Handler { message, .. } => message,
|
|
2953
|
+
AsyncApiError::Middleware { message, .. } => message,
|
|
2954
|
+
AsyncApiError::Recovery { message, .. } => message,
|
|
2955
|
+
AsyncApiError::Resource { message, .. } => message,
|
|
2956
|
+
AsyncApiError::Security { message, .. } => message,
|
|
2957
|
+
AsyncApiError::Authentication { message, .. } => message,
|
|
2958
|
+
AsyncApiError::Authorization { message, .. } => message,
|
|
2959
|
+
AsyncApiError::RateLimit { message, .. } => message,
|
|
2960
|
+
AsyncApiError::Context { message, .. } => message,
|
|
2961
|
+
}
|
|
2962
|
+
}
|
|
2963
|
+
|
|
2964
|
+
/// Get error metadata (if available)
|
|
2965
|
+
pub fn metadata(&self) -> Option<&ErrorMetadata> {
|
|
2966
|
+
match self {
|
|
2967
|
+
AsyncApiError::Configuration { metadata, .. } => Some(metadata),
|
|
2968
|
+
AsyncApiError::Protocol { metadata, .. } => Some(metadata),
|
|
2969
|
+
AsyncApiError::Validation { metadata, .. } => Some(metadata),
|
|
2970
|
+
AsyncApiError::Handler { metadata, .. } => Some(metadata),
|
|
2971
|
+
AsyncApiError::Middleware { metadata, .. } => Some(metadata),
|
|
2972
|
+
AsyncApiError::Recovery { metadata, .. } => Some(metadata),
|
|
2973
|
+
AsyncApiError::Resource { metadata, .. } => Some(metadata),
|
|
2974
|
+
AsyncApiError::Security { metadata, .. } => Some(metadata),
|
|
2975
|
+
AsyncApiError::Authentication { metadata, .. } => Some(metadata),
|
|
2976
|
+
AsyncApiError::Authorization { metadata, .. } => Some(metadata),
|
|
2977
|
+
AsyncApiError::Context { metadata, .. } => Some(metadata),
|
|
2978
|
+
AsyncApiError::RateLimit { .. } => None,
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
/// Check if error is retryable
|
|
2983
|
+
pub fn is_retryable(&self) -> bool {
|
|
2984
|
+
self.metadata().map_or(false, |m| m.retryable)
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
/// Get error severity
|
|
2988
|
+
pub fn severity(&self) -> Option<ErrorSeverity> {
|
|
2989
|
+
self.metadata().map(|m| m.severity)
|
|
2990
|
+
}
|
|
2991
|
+
|
|
2992
|
+
/// Get error category
|
|
2993
|
+
pub fn category(&self) -> Option<ErrorCategory> {
|
|
2994
|
+
self.metadata().map(|m| m.category)
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
/// Get correlation ID for tracing
|
|
2998
|
+
pub fn correlation_id(&self) -> Option<&CorrelationId> {
|
|
2999
|
+
self.metadata().map(|m| &m.correlation_id)
|
|
3000
|
+
}
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
impl std::fmt::Display for AsyncApiError {
|
|
3004
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
3005
|
+
write!(f, "{}", self.message())
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
impl std::error::Error for AsyncApiError {}
|
|
2721
3010
|
|
|
2722
3011
|
/// Unified message envelope for consistent AsyncAPI message format
|
|
2723
3012
|
///
|
|
@@ -2791,17 +3080,9 @@ pub struct MessageEnvelope {
|
|
|
2791
3080
|
pub channel: Option<String>,
|
|
2792
3081
|
/// Transport-level headers (auth, routing, etc.)
|
|
2793
3082
|
pub headers: Option<HashMap<String, String>>,
|
|
2794
|
-
///
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
/// Error information for failed operations
|
|
2799
|
-
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
2800
|
-
pub struct MessageError {
|
|
2801
|
-
/// Error code (e.g., "VALIDATION_ERROR", "TIMEOUT", "UNAUTHORIZED")
|
|
2802
|
-
pub code: String,
|
|
2803
|
-
/// Human-readable error message
|
|
2804
|
-
pub message: String,
|
|
3083
|
+
/// Rich error information if operation failed
|
|
3084
|
+
/// Contains detailed error metadata including severity, category, and correlation
|
|
3085
|
+
pub error: Option<AsyncApiError>,
|
|
2805
3086
|
}
|
|
2806
3087
|
|
|
2807
3088
|
impl MessageEnvelope {
|
|
@@ -2839,11 +3120,10 @@ impl MessageEnvelope {
|
|
|
2839
3120
|
Ok(envelope)
|
|
2840
3121
|
}
|
|
2841
3122
|
|
|
2842
|
-
/// Create an error response envelope
|
|
3123
|
+
/// Create an error response envelope with rich AsyncApiError
|
|
2843
3124
|
pub fn error_response(
|
|
2844
3125
|
operation: &str,
|
|
2845
|
-
|
|
2846
|
-
error_message: &str,
|
|
3126
|
+
error: AsyncApiError,
|
|
2847
3127
|
correlation_id: Option<String>,
|
|
2848
3128
|
) -> Self {
|
|
2849
3129
|
Self {
|
|
@@ -2854,13 +3134,28 @@ impl MessageEnvelope {
|
|
|
2854
3134
|
correlation_id,
|
|
2855
3135
|
channel: None,
|
|
2856
3136
|
headers: None,
|
|
2857
|
-
error: Some(
|
|
2858
|
-
code: error_code.to_string(),
|
|
2859
|
-
message: error_message.to_string(),
|
|
2860
|
-
}),
|
|
3137
|
+
error: Some(error),
|
|
2861
3138
|
}
|
|
2862
3139
|
}
|
|
2863
3140
|
|
|
3141
|
+
/// Create a simple error response envelope (for backward compatibility)
|
|
3142
|
+
pub fn simple_error_response(
|
|
3143
|
+
operation: &str,
|
|
3144
|
+
error_message: &str,
|
|
3145
|
+
correlation_id: Option<String>,
|
|
3146
|
+
) -> Self {
|
|
3147
|
+
let error = AsyncApiError::Handler {
|
|
3148
|
+
message: error_message.to_string(),
|
|
3149
|
+
handler_name: operation.to_string(),
|
|
3150
|
+
metadata: ErrorMetadata::new(
|
|
3151
|
+
ErrorSeverity::High,
|
|
3152
|
+
ErrorCategory::BusinessLogic,
|
|
3153
|
+
false,
|
|
3154
|
+
),
|
|
3155
|
+
};
|
|
3156
|
+
Self::error_response(operation, error, correlation_id)
|
|
3157
|
+
}
|
|
3158
|
+
|
|
2864
3159
|
/// Set the correlation ID for this envelope
|
|
2865
3160
|
pub fn with_correlation_id(mut self, id: String) -> Self {
|
|
2866
3161
|
self.correlation_id = Some(id);
|
|
@@ -2904,11 +3199,23 @@ impl MessageEnvelope {
|
|
|
2904
3199
|
}
|
|
2905
3200
|
|
|
2906
3201
|
/// Set an error on this envelope
|
|
2907
|
-
pub fn with_error(mut self,
|
|
2908
|
-
self.error = Some(
|
|
2909
|
-
|
|
3202
|
+
pub fn with_error(mut self, error: AsyncApiError) -> Self {
|
|
3203
|
+
self.error = Some(error);
|
|
3204
|
+
self
|
|
3205
|
+
}
|
|
3206
|
+
|
|
3207
|
+
/// Set a simple error on this envelope (for backward compatibility)
|
|
3208
|
+
pub fn with_simple_error(mut self, message: &str) -> Self {
|
|
3209
|
+
let error = AsyncApiError::Handler {
|
|
2910
3210
|
message: message.to_string(),
|
|
2911
|
-
|
|
3211
|
+
handler_name: self.operation.clone(),
|
|
3212
|
+
metadata: ErrorMetadata::new(
|
|
3213
|
+
ErrorSeverity::High,
|
|
3214
|
+
ErrorCategory::BusinessLogic,
|
|
3215
|
+
false,
|
|
3216
|
+
),
|
|
3217
|
+
};
|
|
3218
|
+
self.error = Some(error);
|
|
2912
3219
|
self
|
|
2913
3220
|
}
|
|
2914
3221
|
|
|
@@ -2950,134 +3257,6 @@ impl MessageEnvelope {
|
|
|
2950
3257
|
}
|
|
2951
3258
|
}
|
|
2952
3259
|
|
|
2953
|
-
#[cfg(test)]
|
|
2954
|
-
mod tests {
|
|
2955
|
-
use super::*;
|
|
2956
|
-
use serde::{Deserialize, Serialize};
|
|
2957
|
-
|
|
2958
|
-
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
|
2959
|
-
struct TestPayload {
|
|
2960
|
-
message: String,
|
|
2961
|
-
count: u32,
|
|
2962
|
-
}
|
|
2963
|
-
|
|
2964
|
-
#[test]
|
|
2965
|
-
fn test_envelope_creation() {
|
|
2966
|
-
let payload = TestPayload {
|
|
2967
|
-
message: "test".to_string(),
|
|
2968
|
-
count: 42,
|
|
2969
|
-
};
|
|
2970
|
-
|
|
2971
|
-
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
2972
|
-
|
|
2973
|
-
assert_eq!(envelope.operation, "test_operation");
|
|
2974
|
-
assert!(!envelope.id.is_empty());
|
|
2975
|
-
assert!(!envelope.timestamp.is_empty());
|
|
2976
|
-
assert_eq!(envelope.correlation_id, None);
|
|
2977
|
-
assert_eq!(envelope.error, None);
|
|
2978
|
-
|
|
2979
|
-
let extracted: TestPayload = envelope.extract_payload().unwrap();
|
|
2980
|
-
assert_eq!(extracted, payload);
|
|
2981
|
-
}
|
|
2982
|
-
|
|
2983
|
-
#[test]
|
|
2984
|
-
fn test_envelope_with_correlation_id() {
|
|
2985
|
-
let payload = TestPayload {
|
|
2986
|
-
message: "test".to_string(),
|
|
2987
|
-
count: 42,
|
|
2988
|
-
};
|
|
2989
|
-
|
|
2990
|
-
let correlation_id = "test-correlation-id".to_string();
|
|
2991
|
-
let envelope = MessageEnvelope::new_with_correlation_id(
|
|
2992
|
-
"test_operation",
|
|
2993
|
-
&payload,
|
|
2994
|
-
correlation_id.clone(),
|
|
2995
|
-
).unwrap();
|
|
2996
|
-
|
|
2997
|
-
assert_eq!(envelope.correlation_id, Some(correlation_id));
|
|
2998
|
-
}
|
|
2999
|
-
|
|
3000
|
-
#[test]
|
|
3001
|
-
fn test_error_response() {
|
|
3002
|
-
let error_envelope = MessageEnvelope::error_response(
|
|
3003
|
-
"test_operation_response",
|
|
3004
|
-
"TEST_ERROR",
|
|
3005
|
-
"Test error message",
|
|
3006
|
-
Some("correlation-123".to_string()),
|
|
3007
|
-
);
|
|
3008
|
-
|
|
3009
|
-
assert!(error_envelope.is_error());
|
|
3010
|
-
assert_eq!(error_envelope.correlation_id, Some("correlation-123".to_string()));
|
|
3011
|
-
if let Some(error) = &error_envelope.error {
|
|
3012
|
-
assert_eq!(error.code, "TEST_ERROR");
|
|
3013
|
-
assert_eq!(error.message, "Test error message");
|
|
3014
|
-
}
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
#[test]
|
|
3018
|
-
fn test_envelope_serialization() {
|
|
3019
|
-
let payload = TestPayload {
|
|
3020
|
-
message: "test".to_string(),
|
|
3021
|
-
count: 42,
|
|
3022
|
-
};
|
|
3023
|
-
|
|
3024
|
-
let envelope = MessageEnvelope::new("test_operation", &payload).unwrap();
|
|
3025
|
-
let bytes = envelope.to_bytes().unwrap();
|
|
3026
|
-
let deserialized = MessageEnvelope::from_bytes(&bytes).unwrap();
|
|
3027
|
-
|
|
3028
|
-
assert_eq!(envelope.id, deserialized.id);
|
|
3029
|
-
assert_eq!(envelope.operation, deserialized.operation);
|
|
3030
|
-
assert_eq!(envelope.timestamp, deserialized.timestamp);
|
|
3031
|
-
}
|
|
3032
|
-
|
|
3033
|
-
#[test]
|
|
3034
|
-
fn test_response_creation() {
|
|
3035
|
-
let request_payload = TestPayload {
|
|
3036
|
-
message: "request".to_string(),
|
|
3037
|
-
count: 1,
|
|
3038
|
-
};
|
|
3039
|
-
|
|
3040
|
-
let response_payload = TestPayload {
|
|
3041
|
-
message: "response".to_string(),
|
|
3042
|
-
count: 2,
|
|
3043
|
-
};
|
|
3044
|
-
|
|
3045
|
-
let request = MessageEnvelope::new_with_correlation_id(
|
|
3046
|
-
"test_request",
|
|
3047
|
-
&request_payload,
|
|
3048
|
-
"test-correlation".to_string(),
|
|
3049
|
-
).unwrap();
|
|
3050
|
-
|
|
3051
|
-
let response = request.create_response("test_response", &response_payload).unwrap();
|
|
3052
|
-
|
|
3053
|
-
assert_eq!(response.operation, "test_response");
|
|
3054
|
-
assert_eq!(response.correlation_id, request.correlation_id);
|
|
3055
|
-
|
|
3056
|
-
let extracted: TestPayload = response.extract_payload().unwrap();
|
|
3057
|
-
assert_eq!(extracted, response_payload);
|
|
3058
|
-
}
|
|
3059
|
-
|
|
3060
|
-
#[test]
|
|
3061
|
-
fn test_headers_and_auth() {
|
|
3062
|
-
let payload = TestPayload {
|
|
3063
|
-
message: "test".to_string(),
|
|
3064
|
-
count: 42,
|
|
3065
|
-
};
|
|
3066
|
-
|
|
3067
|
-
let mut auth_headers = HashMap::new();
|
|
3068
|
-
auth_headers.insert("Authorization".to_string(), "Bearer token123".to_string());
|
|
3069
|
-
|
|
3070
|
-
let envelope = MessageEnvelope::new("test_operation", &payload)
|
|
3071
|
-
.unwrap()
|
|
3072
|
-
.with_auth_headers(auth_headers)
|
|
3073
|
-
.with_header("Custom-Header".to_string(), "custom-value".to_string());
|
|
3074
|
-
|
|
3075
|
-
assert!(envelope.headers.is_some());
|
|
3076
|
-
let headers = envelope.headers.unwrap();
|
|
3077
|
-
assert_eq!(headers.get("Authorization"), Some(&"Bearer token123".to_string()));
|
|
3078
|
-
assert_eq!(headers.get("Custom-Header"), Some(&"custom-value".to_string()));
|
|
3079
|
-
}
|
|
3080
|
-
}
|
|
3081
3260
|
`;
|
|
3082
3261
|
}
|
|
3083
3262
|
;// ../common/src/models-ts.js
|
package/package.json
CHANGED
|
@@ -190,6 +190,7 @@ export default function ClientRs({ asyncapi, params }) {
|
|
|
190
190
|
/// * \`ClientError::Nats\` - NATS operation failed
|
|
191
191
|
/// * \`ClientError::Serialization\` - Failed to serialize/deserialize data
|
|
192
192
|
/// * \`ClientError::Timeout\` - Request timed out
|
|
193
|
+
/// * \`ClientError::AsyncApi\` - Server returned an error
|
|
193
194
|
pub async fn ${op.methodName}(&self, payload: ${op.requestType}) -> ClientResult<${op.responseType}> {
|
|
194
195
|
let envelope = if let Some(ref auth) = self.auth {
|
|
195
196
|
MessageEnvelope::new_with_auth("${op.operationName}", payload, auth)
|
|
@@ -208,6 +209,11 @@ export default function ClientRs({ asyncapi, params }) {
|
|
|
208
209
|
let response_envelope = MessageEnvelope::from_bytes(&response.payload)
|
|
209
210
|
.map_err(|e| ClientError::InvalidEnvelope(e.to_string()))?;
|
|
210
211
|
|
|
212
|
+
// Check if the response contains an error
|
|
213
|
+
if let Some(error) = response_envelope.error {
|
|
214
|
+
return Err(ClientError::AsyncApi(Box::new(error)));
|
|
215
|
+
}
|
|
216
|
+
|
|
211
217
|
let result: ${op.responseType} = response_envelope.extract_payload()
|
|
212
218
|
.map_err(ClientError::Serialization)?;
|
|
213
219
|
|
|
@@ -399,6 +405,7 @@ ${serviceOperations}
|
|
|
399
405
|
/// * \`ClientError::Nats\` - NATS operation failed
|
|
400
406
|
/// * \`ClientError::Serialization\` - Failed to serialize/deserialize data
|
|
401
407
|
/// * \`ClientError::Timeout\` - Request timed out
|
|
408
|
+
/// * \`ClientError::AsyncApi\` - Server returned an error
|
|
402
409
|
pub async fn ${pattern.methodName}(&self, payload: ${pattern.requestType}) -> ClientResult<${pattern.responseType}> {
|
|
403
410
|
let envelope = if let Some(ref auth) = self.auth {
|
|
404
411
|
MessageEnvelope::new_with_auth("${pattern.operationName}", payload, auth)
|
|
@@ -416,6 +423,11 @@ ${serviceOperations}
|
|
|
416
423
|
let response_envelope = MessageEnvelope::from_bytes(&response.payload)
|
|
417
424
|
.map_err(|e| ClientError::InvalidEnvelope(e.to_string()))?;
|
|
418
425
|
|
|
426
|
+
// Check if the response contains an error
|
|
427
|
+
if let Some(error) = response_envelope.error {
|
|
428
|
+
return Err(ClientError::AsyncApi(Box::new(error)));
|
|
429
|
+
}
|
|
430
|
+
|
|
419
431
|
let result: ${pattern.responseType} = response_envelope.extract_payload()
|
|
420
432
|
.map_err(ClientError::Serialization)?;
|
|
421
433
|
|
|
@@ -1,196 +1,28 @@
|
|
|
1
1
|
/* eslint-disable no-unused-vars */
|
|
2
2
|
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
import { generateMessageEnvelope } from "../../dist/common/index.js";
|
|
3
4
|
|
|
4
5
|
export default function ({ asyncapi, params }) {
|
|
6
|
+
// Generate the unified message envelope with error support
|
|
7
|
+
const envelopeCode = generateMessageEnvelope();
|
|
8
|
+
|
|
5
9
|
return (
|
|
6
10
|
<File name="envelope.rs">
|
|
7
11
|
{`//! Message envelope for consistent NATS message format
|
|
8
12
|
|
|
9
13
|
use crate::auth::{AuthCredentials, generate_auth_headers};
|
|
10
|
-
|
|
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
|
-
}
|
|
14
|
+
${envelopeCode}
|
|
31
15
|
|
|
16
|
+
// Client-specific extensions for auth integration
|
|
32
17
|
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
18
|
/// Create a new message envelope with authentication headers
|
|
93
19
|
pub fn new_with_auth<T: Serialize>(
|
|
94
20
|
operation: &str,
|
|
95
21
|
payload: T,
|
|
96
22
|
auth: &AuthCredentials,
|
|
97
23
|
) -> Result<Self, serde_json::Error> {
|
|
98
|
-
let
|
|
99
|
-
|
|
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);
|
|
24
|
+
let auth_headers = generate_auth_headers(auth);
|
|
25
|
+
Self::new(operation, payload).map(|envelope| envelope.with_auth_headers(auth_headers))
|
|
194
26
|
}
|
|
195
27
|
}
|
|
196
28
|
`}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/* eslint-disable no-unused-vars */
|
|
2
|
+
import { File } from '@asyncapi/generator-react-sdk';
|
|
3
|
+
import { generateMessageEnvelope } from '../../../common/src/index.js';
|
|
4
|
+
|
|
5
|
+
export default function ({ asyncapi, params }) {
|
|
6
|
+
// Generate the unified message envelope with error support
|
|
7
|
+
const envelopeCode = generateMessageEnvelope();
|
|
8
|
+
|
|
9
|
+
return (
|
|
10
|
+
<File name="envelope.rs">
|
|
11
|
+
{`//! Message envelope for consistent NATS message format
|
|
12
|
+
|
|
13
|
+
use crate::auth::{AuthCredentials, generate_auth_headers};
|
|
14
|
+
${envelopeCode}
|
|
15
|
+
|
|
16
|
+
// Client-specific extensions for auth integration
|
|
17
|
+
impl MessageEnvelope {
|
|
18
|
+
/// Create a new message envelope with authentication headers
|
|
19
|
+
pub fn new_with_auth<T: Serialize>(
|
|
20
|
+
operation: &str,
|
|
21
|
+
payload: T,
|
|
22
|
+
auth: &AuthCredentials,
|
|
23
|
+
) -> Result<Self, serde_json::Error> {
|
|
24
|
+
let auth_headers = generate_auth_headers(auth);
|
|
25
|
+
Self::new(operation, payload).map(|envelope| envelope.with_auth_headers(auth_headers))
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
`}
|
|
29
|
+
</File>
|
|
30
|
+
);
|
|
31
|
+
};
|
|
@@ -9,6 +9,9 @@ export default function ({ asyncapi, params }) {
|
|
|
9
9
|
use crate::auth::AuthError;
|
|
10
10
|
use thiserror::Error;
|
|
11
11
|
|
|
12
|
+
// Import the shared AsyncApiError type from the envelope/models module
|
|
13
|
+
pub use crate::envelope::{AsyncApiError, ErrorSeverity, ErrorCategory, ErrorMetadata, CorrelationId as ErrorCorrelationId};
|
|
14
|
+
|
|
12
15
|
/// Errors that can occur when using the NATS client
|
|
13
16
|
#[derive(Debug, Error)]
|
|
14
17
|
pub enum ClientError {
|
|
@@ -39,6 +42,58 @@ pub enum ClientError {
|
|
|
39
42
|
/// Unauthorized access
|
|
40
43
|
#[error("Unauthorized: {0}")]
|
|
41
44
|
Unauthorized(String),
|
|
45
|
+
|
|
46
|
+
/// Server-side AsyncAPI error
|
|
47
|
+
///
|
|
48
|
+
/// This error contains rich information from the server including:
|
|
49
|
+
/// - Error severity and category
|
|
50
|
+
/// - Correlation ID for tracing
|
|
51
|
+
/// - Detailed error metadata
|
|
52
|
+
/// - Whether the error is retryable
|
|
53
|
+
#[error("Server error: {0}")]
|
|
54
|
+
AsyncApi(Box<AsyncApiError>),
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
impl ClientError {
|
|
58
|
+
/// Check if this error is retryable
|
|
59
|
+
pub fn is_retryable(&self) -> bool {
|
|
60
|
+
match self {
|
|
61
|
+
ClientError::Nats(_) => true,
|
|
62
|
+
ClientError::Timeout => true,
|
|
63
|
+
ClientError::NoResponse => true,
|
|
64
|
+
ClientError::AsyncApi(err) => err.is_retryable(),
|
|
65
|
+
_ => false,
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/// Get error severity if available
|
|
70
|
+
pub fn severity(&self) -> Option<ErrorSeverity> {
|
|
71
|
+
match self {
|
|
72
|
+
ClientError::AsyncApi(err) => err.severity(),
|
|
73
|
+
_ => None,
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/// Get error category if available
|
|
78
|
+
pub fn category(&self) -> Option<ErrorCategory> {
|
|
79
|
+
match self {
|
|
80
|
+
ClientError::AsyncApi(err) => err.category(),
|
|
81
|
+
_ => None,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/// Get correlation ID for tracing if available
|
|
86
|
+
pub fn correlation_id(&self) -> Option<&ErrorCorrelationId> {
|
|
87
|
+
match self {
|
|
88
|
+
ClientError::AsyncApi(err) => err.correlation_id(),
|
|
89
|
+
_ => None,
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/// Check if this is a server-side error
|
|
94
|
+
pub fn is_server_error(&self) -> bool {
|
|
95
|
+
matches!(self, ClientError::AsyncApi(_))
|
|
96
|
+
}
|
|
42
97
|
}
|
|
43
98
|
|
|
44
99
|
/// Result type alias for client operations
|