airsprotocols_mcp/protocol/transport.rs
1//! MCP Transport Abstractions and Event-Driven Architecture
2//!
3//! This module provides the event-driven transport abstraction aligned with the
4//! official Model Context Protocol (MCP) specification, providing sophisticated
5//! message handling that matches the patterns used in official TypeScript and Python SDKs.
6//!
7//! # Architecture
8//!
9//! The MCP-compliant transport layer is built around:
10//! - **MessageHandler**: Event-driven protocol logic (separation of concerns)
11//! - **Transport**: Event-driven transport interface
12//! - **MessageContext**: Session and metadata management
13//! - **TransportError**: Comprehensive error handling
14//!
15//! # Design Philosophy
16//!
17//! - **Event-Driven**: Uses callbacks instead of blocking receive() operations
18//! - **Specification-Aligned**: Matches official MCP SDK patterns exactly
19//! - **Clean Separation**: Transport handles delivery, MessageHandler handles protocol
20//! - **Natural Correlation**: Uses JSON-RPC message IDs, no artificial mechanisms
21//! - **Session-Aware**: Supports multi-session transports like HTTP
22//!
23//! # Examples
24//!
25//! ## Generic MessageHandler Pattern
26//!
27//! ```rust
28//! use airsprotocols_mcp::protocol::{MessageHandler, JsonRpcMessage, MessageContext, TransportError};
29//! use std::sync::Arc;
30//! use async_trait::async_trait;
31//!
32//! // Example with unit context (for STDIO transport)
33//! struct EchoHandler;
34//!
35//! #[async_trait]
36//! impl MessageHandler<()> for EchoHandler {
37//! async fn handle_message(&self, message: JsonRpcMessage, _context: MessageContext<()>) {
38//! println!("Received message: {:?}", message);
39//! }
40//!
41//! async fn handle_error(&self, error: TransportError) {
42//! eprintln!("Transport error: {:?}", error);
43//! }
44//!
45//! async fn handle_close(&self) {
46//! println!("Transport closed");
47//! }
48//! }
49//! ```
50
51// Layer 1: Standard library imports
52use std::collections::HashMap;
53use std::time::Duration;
54
55// Layer 2: Third-party crate imports
56use async_trait::async_trait;
57use chrono::{DateTime, Utc};
58use thiserror::Error;
59
60// Layer 3: Internal module imports
61use super::message::{JsonRpcMessage, JsonRpcRequest, JsonRpcResponse};
62use super::types::{ProtocolVersion, ServerCapabilities, ServerConfig, ServerInfo};
63
64/// Transport error types for comprehensive error handling
65///
66/// This enum covers all possible transport-level errors that can occur
67/// during MCP message handling, providing specific error types for
68/// different failure scenarios.
69#[derive(Error, Debug)]
70pub enum TransportError {
71 /// Connection-related errors
72 #[error("Connection error: {message}")]
73 Connection { message: String },
74
75 /// I/O operation errors
76 #[error("I/O error: {source}")]
77 Io { source: std::io::Error },
78
79 /// Message serialization/deserialization errors
80 #[error("Serialization error: {source}")]
81 Serialization { source: serde_json::Error },
82
83 /// Protocol-level errors
84 #[error("Protocol error: {message}")]
85 Protocol { message: String },
86
87 /// Timeout errors
88 #[error("Timeout error: {message}")]
89 Timeout { message: String },
90
91 /// Authentication/authorization errors
92 #[error("Authentication error: {message}")]
93 Auth { message: String },
94
95 /// Request-specific timeout errors (client operation)
96 #[error("Request timeout after {duration:?}")]
97 RequestTimeout { duration: Duration },
98
99 /// Response parsing errors (client receiving invalid response)
100 #[error("Invalid response format: {message}")]
101 InvalidResponse { message: String },
102
103 /// Client not ready for requests (not connected, initializing, etc.)
104 #[error("Client not ready: {reason}")]
105 NotReady { reason: String },
106
107 /// Generic transport errors
108 #[error("Transport error: {message}")]
109 Other { message: String },
110}
111
112impl From<std::io::Error> for TransportError {
113 fn from(error: std::io::Error) -> Self {
114 TransportError::Io { source: error }
115 }
116}
117
118impl From<serde_json::Error> for TransportError {
119 fn from(error: serde_json::Error) -> Self {
120 TransportError::Serialization { source: error }
121 }
122}
123
124impl TransportError {
125 /// Create a request timeout error with the specified duration
126 ///
127 /// This convenience constructor is useful for client implementations
128 /// when a request times out after waiting for the specified duration.
129 ///
130 /// # Arguments
131 ///
132 /// * `duration` - The duration after which the request timed out
133 ///
134 /// # Examples
135 ///
136 /// ```rust
137 /// use airsprotocols_mcp::protocol::TransportError;
138 /// use std::time::Duration;
139 ///
140 /// let timeout_error = TransportError::request_timeout(Duration::from_secs(30));
141 /// ```
142 pub fn request_timeout(duration: Duration) -> Self {
143 Self::RequestTimeout { duration }
144 }
145
146 /// Create an invalid response error with the specified message
147 ///
148 /// This convenience constructor is useful for client implementations
149 /// when they receive a response that cannot be parsed or is malformed.
150 ///
151 /// # Arguments
152 ///
153 /// * `message` - Description of what made the response invalid
154 ///
155 /// # Examples
156 ///
157 /// ```rust
158 /// use airsprotocols_mcp::protocol::TransportError;
159 ///
160 /// let invalid_response = TransportError::invalid_response("Missing required 'result' field");
161 /// ```
162 pub fn invalid_response(message: impl Into<String>) -> Self {
163 Self::InvalidResponse {
164 message: message.into(),
165 }
166 }
167
168 /// Create a not ready error with the specified reason
169 ///
170 /// This convenience constructor is useful for client implementations
171 /// when they cannot process requests due to not being ready.
172 ///
173 /// # Arguments
174 ///
175 /// * `reason` - Description of why the client is not ready
176 ///
177 /// # Examples
178 ///
179 /// ```rust
180 /// use airsprotocols_mcp::protocol::TransportError;
181 ///
182 /// let not_ready = TransportError::not_ready("Transport not connected");
183 /// ```
184 pub fn not_ready(reason: impl Into<String>) -> Self {
185 Self::NotReady {
186 reason: reason.into(),
187 }
188 }
189}
190
191/// Message context for session and metadata management
192///
193/// This structure carries session information and metadata for each message,
194/// enabling proper handling of multi-session transports like HTTP.
195///
196/// # Examples
197///
198/// ```rust
199/// use airsprotocols_mcp::protocol::MessageContext;
200/// use chrono::Utc;
201///
202/// // Default generic context (for STDIO)
203/// let context = MessageContext::<()>::new("session-123".to_string())
204/// .with_remote_addr("192.168.1.100:8080".to_string())
205/// .with_user_agent("airsprotocols-mcp-client/1.0".to_string());
206///
207/// assert_eq!(context.session_id(), Some("session-123"));
208/// assert_eq!(context.remote_addr(), Some("192.168.1.100:8080"));
209/// ```
210#[derive(Debug, Clone)]
211pub struct MessageContext<T = ()> {
212 /// Session identifier (if applicable)
213 session_id: Option<String>,
214
215 /// Timestamp when message was received
216 timestamp: DateTime<Utc>,
217
218 /// Remote address/endpoint information
219 remote_addr: Option<String>,
220
221 /// Additional metadata
222 metadata: HashMap<String, String>,
223
224 /// Transport-specific data (generic for different transport types)
225 transport_data: Option<T>,
226}
227
228impl<T> MessageContext<T> {
229 /// Create a new message context with transport-specific data
230 pub fn new_with_transport_data(session_id: impl Into<String>, transport_data: T) -> Self {
231 Self {
232 session_id: Some(session_id.into()),
233 timestamp: Utc::now(),
234 remote_addr: None,
235 metadata: HashMap::new(),
236 transport_data: Some(transport_data),
237 }
238 }
239
240 /// Create a new message context without transport data (for simple transports)
241 pub fn new(session_id: impl Into<String>) -> Self
242 where
243 T: Default,
244 {
245 Self {
246 session_id: Some(session_id.into()),
247 timestamp: Utc::now(),
248 remote_addr: None,
249 metadata: HashMap::new(),
250 transport_data: None,
251 }
252 }
253
254 /// Create a new message context without session ID or transport data
255 pub fn without_session() -> Self
256 where
257 T: Default,
258 {
259 Self {
260 session_id: None,
261 timestamp: Utc::now(),
262 remote_addr: None,
263 metadata: HashMap::new(),
264 transport_data: None,
265 }
266 }
267
268 /// Get session ID
269 pub fn session_id(&self) -> Option<&str> {
270 self.session_id.as_deref()
271 }
272
273 /// Get message timestamp
274 pub fn timestamp(&self) -> DateTime<Utc> {
275 self.timestamp
276 }
277
278 /// Get remote address
279 pub fn remote_addr(&self) -> Option<&str> {
280 self.remote_addr.as_deref()
281 }
282
283 /// Get metadata value
284 pub fn get_metadata(&self, key: &str) -> Option<&str> {
285 self.metadata.get(key).map(|s| s.as_str())
286 }
287
288 /// Set remote address
289 pub fn with_remote_addr(mut self, addr: String) -> Self {
290 self.remote_addr = Some(addr);
291 self
292 }
293
294 /// Add metadata
295 pub fn with_metadata(mut self, key: String, value: String) -> Self {
296 self.metadata.insert(key, value);
297 self
298 }
299
300 /// Convenience method to add user agent
301 pub fn with_user_agent(self, user_agent: String) -> Self {
302 self.with_metadata("user-agent".to_string(), user_agent)
303 }
304
305 /// Convenience method to add content type
306 pub fn with_content_type(self, content_type: String) -> Self {
307 self.with_metadata("content-type".to_string(), content_type)
308 }
309
310 /// Get transport-specific data
311 ///
312 /// Returns a reference to the transport-specific data if it exists.
313 /// This allows handlers to access transport-specific context information.
314 pub fn transport_data(&self) -> Option<&T> {
315 self.transport_data.as_ref()
316 }
317
318 /// Set transport-specific data
319 ///
320 /// Adds or updates transport-specific data for this context.
321 pub fn with_transport_data(mut self, data: T) -> Self {
322 self.transport_data = Some(data);
323 self
324 }
325
326 /// Check if transport data is available
327 ///
328 /// Returns true if this context contains transport-specific data.
329 pub fn has_transport_data(&self) -> bool {
330 self.transport_data.is_some()
331 }
332}
333
334/// Event-driven message handler trait
335///
336/// This trait defines the interface for handling MCP protocol logic,
337/// providing clean separation between transport (message delivery) and
338/// protocol (MCP semantics) concerns.
339///
340/// The event-driven design matches the official MCP specification patterns
341/// and eliminates the complexity of blocking receive() operations.
342///
343/// The generic type parameter `T` represents transport-specific context data
344/// that can be included with each message (e.g., HTTP request details).
345///
346/// # Examples
347///
348/// ```rust
349/// use airsprotocols_mcp::protocol::{MessageHandler, JsonRpcMessage, MessageContext, TransportError};
350/// use async_trait::async_trait;
351/// use std::sync::Arc;
352///
353/// struct EchoHandler;
354///
355/// #[async_trait]
356/// impl MessageHandler<()> for EchoHandler {
357/// async fn handle_message(&self, message: JsonRpcMessage, context: MessageContext<()>) {
358/// println!("Received message: {:?}", message);
359/// // Echo logic would go here
360/// }
361///
362/// async fn handle_error(&self, error: TransportError) {
363/// eprintln!("Transport error: {}", error);
364/// }
365///
366/// async fn handle_close(&self) {
367/// println!("Transport closed");
368/// }
369/// }
370/// ```
371#[async_trait]
372pub trait MessageHandler<T = ()>: Send + Sync {
373 /// Handle an incoming JSON-RPC message
374 ///
375 /// This method is called for every message received by the transport,
376 /// including requests, responses, and notifications.
377 ///
378 /// # Arguments
379 ///
380 /// * `message` - The JSON-RPC message received
381 /// * `context` - Session and metadata information with transport-specific data
382 async fn handle_message(&self, message: JsonRpcMessage, context: MessageContext<T>);
383
384 /// Handle a transport-level error
385 ///
386 /// This method is called when the transport encounters an error that
387 /// doesn't result in a valid JSON-RPC message (e.g., connection failures).
388 ///
389 /// # Arguments
390 ///
391 /// * `error` - The transport error that occurred
392 async fn handle_error(&self, error: TransportError);
393
394 /// Handle transport closure
395 ///
396 /// This method is called when the transport is closed, either gracefully
397 /// or due to an error. It provides an opportunity for cleanup.
398 async fn handle_close(&self);
399}
400
401/// MCP-compliant transport trait
402///
403/// This trait defines the event-driven transport interface aligned with the
404/// official MCP specification. It replaces the blocking receive() pattern
405/// with event-driven message handling via MessageHandler callbacks.
406///
407/// # Design Principles
408///
409/// - **Event-Driven**: Uses MessageHandler callbacks instead of blocking receive()
410/// - **Session-Aware**: Supports multi-session transports (e.g., HTTP)
411/// - **Lifecycle Management**: Explicit start/close for resource management
412/// - **Natural Correlation**: Uses JSON-RPC message IDs, no artificial mechanisms
413/// - **Transport Agnostic**: Works with STDIO, HTTP, WebSocket, etc.
414///
415/// # Examples
416///
417/// See specific transport implementations:
418/// - `transport::adapters::stdio::StdioTransport` for STDIO communication
419/// - `transport::adapters::http::HttpTransport` for HTTP-based communication
420///
421/// ```rust
422/// use airsprotocols_mcp::protocol::{Transport, JsonRpcMessage};
423/// use async_trait::async_trait;
424///
425/// // Transport implementations provide event-driven message handling
426/// // via the MessageHandler pattern - see transport adapters for examples
427/// ```
428#[async_trait]
429pub trait Transport: Send + Sync {
430 /// Transport-specific error type
431 type Error: std::error::Error + Send + Sync + 'static;
432
433 /// Start the transport and begin listening for messages
434 ///
435 /// This method initializes the transport and begins accepting incoming
436 /// messages. For connection-based transports, this establishes the connection.
437 /// For server transports, this starts listening for connections.
438 ///
439 /// # Returns
440 ///
441 /// * `Ok(())` - Transport started successfully
442 /// * `Err(Self::Error)` - Failed to start transport
443 async fn start(&mut self) -> Result<(), Self::Error>;
444
445 /// Close the transport and clean up resources
446 ///
447 /// This method gracefully shuts down the transport, closes connections,
448 /// and releases resources. It should be idempotent.
449 ///
450 /// # Returns
451 ///
452 /// * `Ok(())` - Transport closed successfully
453 /// * `Err(Self::Error)` - Error during closure (resources may still be cleaned up)
454 async fn close(&mut self) -> Result<(), Self::Error>;
455
456 /// Send a JSON-RPC message through the transport
457 ///
458 /// This method sends a message through the transport. For connection-based
459 /// transports, this sends over the active connection. For HTTP transports,
460 /// this may initiate a new request/response cycle.
461 ///
462 /// # Arguments
463 ///
464 /// * `message` - JSON-RPC message to send
465 ///
466 /// # Returns
467 ///
468 /// * `Ok(())` - Message sent successfully
469 /// * `Err(Self::Error)` - Failed to send message
470 async fn send(&mut self, message: &JsonRpcMessage) -> Result<(), Self::Error>;
471
472 /// Get the current session ID (if applicable)
473 ///
474 /// For session-based transports, this returns the current session identifier.
475 /// For single-connection transports like STDIO, this may return None.
476 ///
477 /// # Returns
478 ///
479 /// * `Some(String)` - Current session ID
480 /// * `None` - No session or single-connection transport
481 fn session_id(&self) -> Option<String>;
482
483 /// Set session context for the transport
484 ///
485 /// For session-based transports, this sets the current session context.
486 /// This method allows the transport to track which session is being used.
487 ///
488 /// # Arguments
489 ///
490 /// * `session_id` - Session identifier to set (None to clear)
491 fn set_session_context(&mut self, session_id: Option<String>);
492
493 /// Check if the transport is currently connected
494 ///
495 /// # Returns
496 ///
497 /// * `true` - Transport is connected and ready to send/receive
498 /// * `false` - Transport is disconnected or not ready
499 fn is_connected(&self) -> bool;
500
501 /// Get the transport type identifier
502 ///
503 /// This returns a string identifying the transport type (e.g., "stdio", "http", "websocket").
504 /// Useful for logging and debugging.
505 ///
506 /// # Returns
507 ///
508 /// Static string identifying the transport type
509 fn transport_type(&self) -> &'static str;
510}
511
512/// Client-oriented transport interface for request-response communication
513///
514/// This trait provides a clean, synchronous interface for client-side MCP communication,
515/// focusing on the natural request-response pattern that clients expect. Unlike the
516/// server-oriented `Transport` trait which uses event-driven `MessageHandler` patterns,
517/// `TransportClient` provides direct request-response semantics.
518///
519/// # Design Philosophy
520///
521/// - **Request-Response Pattern**: Direct mapping to client mental model
522/// - **Synchronous Flow**: No complex correlation mechanisms needed
523/// - **Simple Interface**: Single method for core operation
524/// - **Transport Agnostic**: Each implementation handles its own details
525/// - **Error Clarity**: Clear error types for client scenarios
526///
527/// # Examples
528///
529/// ```rust
530/// use airsprotocols_mcp::protocol::{TransportClient, JsonRpcRequest, RequestId};
531/// use serde_json::json;
532///
533/// async fn example_usage<T: TransportClient>(mut client: T) -> Result<(), Box<dyn std::error::Error>> {
534/// // Create a request
535/// let request = JsonRpcRequest::new(
536/// "initialize",
537/// Some(json!({"capabilities": {}})),
538/// RequestId::new_string("init-1")
539/// );
540///
541/// // Send request and receive response directly
542/// let response = client.call(request).await?;
543///
544/// println!("Received response: {:?}", response);
545/// Ok(())
546/// }
547/// ```
548#[async_trait]
549pub trait TransportClient: Send + Sync {
550 /// Transport-specific error type
551 type Error: std::error::Error + Send + Sync + 'static;
552
553 /// Send a JSON-RPC request and receive the response
554 ///
555 /// This is the core method of the client interface, providing direct
556 /// request-response semantics. The implementation handles all transport-specific
557 /// details including connection management, serialization, and correlation.
558 ///
559 /// # Arguments
560 ///
561 /// * `request` - The JSON-RPC request to send
562 ///
563 /// # Returns
564 ///
565 /// * `Ok(JsonRpcResponse)` - The response from the server
566 /// * `Err(Self::Error)` - Transport or protocol error
567 ///
568 /// # Error Handling
569 ///
570 /// Implementations should map transport-specific errors to appropriate
571 /// error types, providing clear context for debugging and error handling.
572 async fn call(&mut self, request: JsonRpcRequest) -> Result<JsonRpcResponse, Self::Error>;
573
574 /// Check if the transport client is ready to send requests
575 ///
576 /// This method allows callers to verify that the transport is in a state
577 /// where requests can be sent successfully.
578 ///
579 /// # Returns
580 ///
581 /// * `true` - Client is ready to send requests
582 /// * `false` - Client is not ready (not connected, initializing, etc.)
583 fn is_ready(&self) -> bool;
584
585 /// Get the transport type identifier
586 ///
587 /// Returns a string identifying the transport type for logging and debugging.
588 ///
589 /// # Returns
590 ///
591 /// Static string identifying the transport type (e.g., "stdio", "http")
592 fn transport_type(&self) -> &'static str;
593
594 /// Close the client transport and clean up resources
595 ///
596 /// This method gracefully shuts down the client transport and releases
597 /// any resources. It should be idempotent and safe to call multiple times.
598 ///
599 /// # Returns
600 ///
601 /// * `Ok(())` - Transport closed successfully
602 /// * `Err(Self::Error)` - Error during closure (resources may still be cleaned up)
603 async fn close(&mut self) -> Result<(), Self::Error>;
604}
605
606/// Type alias for boxed transport clients with standardized error handling
607///
608/// This type alias provides a convenient way to work with transport clients
609/// when you need trait objects, using the standard `TransportError` type.
610///
611/// # Examples
612///
613/// ```rust
614/// use airsprotocols_mcp::protocol::{BoxedTransportClient, TransportError};
615///
616/// async fn use_any_client(mut client: BoxedTransportClient) -> Result<(), TransportError> {
617/// // Work with any transport client implementation
618/// // ...
619/// Ok(())
620/// }
621/// ```
622pub type BoxedTransportClient = Box<dyn TransportClient<Error = TransportError>>;
623
624/// Result type for transport client operations
625///
626/// This type alias provides a convenient shorthand for transport client results
627/// using the standard `TransportError` type.
628///
629/// # Examples
630///
631/// ```rust
632/// use airsprotocols_mcp::protocol::{TransportClientResult, JsonRpcResponse};
633///
634/// fn process_response(response: TransportClientResult<JsonRpcResponse>) {
635/// match response {
636/// Ok(resp) => println!("Success: {:?}", resp),
637/// Err(err) => eprintln!("Error: {}", err),
638/// }
639/// }
640/// ```
641pub type TransportClientResult<T> = Result<T, TransportError>;
642
643/// Transport configuration trait for type-safe transport settings
644///
645/// This trait provides a standardized interface for transport-specific configuration
646/// management while maintaining access to universal MCP core requirements.
647///
648/// Reference: ADR-011 Transport Configuration Separation
649pub trait TransportConfig: Send + Sync {
650 /// Set or update the MCP server configuration
651 ///
652 /// This method allows updating the core MCP requirements (server info,
653 /// capabilities, protocol version) that are common across all transports.
654 fn set_server_config(&mut self, server_config: ServerConfig);
655
656 /// Get reference to the MCP server configuration
657 ///
658 /// Returns None if no server configuration has been set.
659 fn server_config(&self) -> Option<&ServerConfig>;
660
661 /// Get effective MCP capabilities for this transport
662 ///
663 /// This method combines the base capabilities from the server config
664 /// with any transport-specific capability modifications.
665 fn effective_capabilities(&self) -> ServerCapabilities;
666
667 /// Get server info from server config
668 ///
669 /// Convenience method that extracts server info from the server config.
670 fn server_info(&self) -> Option<&ServerInfo> {
671 self.server_config().map(|c| &c.server_info)
672 }
673
674 /// Get protocol version from server config
675 ///
676 /// Convenience method that extracts protocol version from the server config.
677 fn protocol_version(&self) -> Option<&ProtocolVersion> {
678 self.server_config().map(|c| &c.protocol_version)
679 }
680}
681
682#[cfg(test)]
683mod tests {
684 use super::*;
685 use crate::protocol::message::{JsonRpcRequest, JsonRpcResponse, RequestId};
686 use serde_json::json;
687
688 /// Mock TransportClient implementation for testing
689 ///
690 /// This simple mock demonstrates that the TransportClient trait
691 /// provides a clean, implementable interface for client operations.
692 struct MockTransportClient {
693 ready: bool,
694 should_fail: bool,
695 }
696
697 impl MockTransportClient {
698 fn new() -> Self {
699 Self {
700 ready: true,
701 should_fail: false,
702 }
703 }
704
705 fn with_failure() -> Self {
706 Self {
707 ready: true,
708 should_fail: true,
709 }
710 }
711
712 fn not_ready() -> Self {
713 Self {
714 ready: false,
715 should_fail: false,
716 }
717 }
718 }
719
720 #[async_trait]
721 impl TransportClient for MockTransportClient {
722 type Error = TransportError;
723
724 async fn call(
725 &mut self,
726 request: JsonRpcRequest,
727 ) -> Result<JsonRpcResponse, TransportError> {
728 if !self.ready {
729 return Err(TransportError::not_ready("Mock client not ready"));
730 }
731
732 if self.should_fail {
733 return Err(TransportError::request_timeout(Duration::from_secs(30)));
734 }
735
736 // Mock successful response
737 Ok(JsonRpcResponse {
738 jsonrpc: "2.0".to_string(),
739 id: Some(request.id),
740 result: Some(json!({"status": "success", "method": request.method})),
741 error: None,
742 })
743 }
744
745 fn is_ready(&self) -> bool {
746 self.ready
747 }
748
749 fn transport_type(&self) -> &'static str {
750 "mock"
751 }
752
753 async fn close(&mut self) -> Result<(), TransportError> {
754 self.ready = false;
755 Ok(())
756 }
757 }
758
759 #[tokio::test]
760 async fn test_transport_client_basic_call() {
761 let mut client = MockTransportClient::new();
762
763 assert!(client.is_ready());
764 assert_eq!(client.transport_type(), "mock");
765
766 let request = JsonRpcRequest::new(
767 "test_method",
768 Some(json!({"param": "value"})),
769 RequestId::new_string("test-1"),
770 );
771
772 let response = client.call(request.clone()).await.unwrap();
773
774 assert_eq!(response.jsonrpc, "2.0");
775 assert_eq!(response.id, Some(request.id));
776 assert!(response.result.is_some());
777 assert!(response.error.is_none());
778 }
779
780 #[tokio::test]
781 async fn test_transport_client_not_ready() {
782 let mut client = MockTransportClient::not_ready();
783
784 assert!(!client.is_ready());
785
786 let request = JsonRpcRequest::new("test_method", None, RequestId::new_string("test-2"));
787
788 let result = client.call(request).await;
789 assert!(result.is_err());
790
791 if let Err(TransportError::NotReady { reason }) = result {
792 assert_eq!(reason, "Mock client not ready");
793 } else {
794 panic!("Expected NotReady error");
795 }
796 }
797
798 #[tokio::test]
799 async fn test_transport_client_timeout() {
800 let mut client = MockTransportClient::with_failure();
801
802 let request = JsonRpcRequest::new("test_method", None, RequestId::new_string("test-3"));
803
804 let result = client.call(request).await;
805 assert!(result.is_err());
806
807 if let Err(TransportError::RequestTimeout { duration }) = result {
808 assert_eq!(duration, Duration::from_secs(30));
809 } else {
810 panic!("Expected RequestTimeout error");
811 }
812 }
813
814 #[tokio::test]
815 async fn test_transport_client_close() {
816 let mut client = MockTransportClient::new();
817
818 assert!(client.is_ready());
819
820 client.close().await.unwrap();
821
822 assert!(!client.is_ready());
823 }
824
825 #[test]
826 fn test_convenience_error_constructors() {
827 let timeout_error = TransportError::request_timeout(Duration::from_secs(10));
828 if let TransportError::RequestTimeout { duration } = timeout_error {
829 assert_eq!(duration, Duration::from_secs(10));
830 } else {
831 panic!("Expected RequestTimeout error");
832 }
833
834 let invalid_response = TransportError::invalid_response("Bad JSON");
835 if let TransportError::InvalidResponse { message } = invalid_response {
836 assert_eq!(message, "Bad JSON");
837 } else {
838 panic!("Expected InvalidResponse error");
839 }
840
841 let not_ready = TransportError::not_ready("Connecting");
842 if let TransportError::NotReady { reason } = not_ready {
843 assert_eq!(reason, "Connecting");
844 } else {
845 panic!("Expected NotReady error");
846 }
847 }
848}