Skip to main content

lsp_server_tokio/
routing.rs

1//! Message routing infrastructure for LSP communication.
2//!
3//! This module provides types and functions for classifying incoming messages
4//! and routing responses to pending outgoing requests.
5//!
6//! # Overview
7//!
8//! The routing infrastructure handles two main concerns:
9//!
10//! 1. **Message Classification**: The [`IncomingMessage`] enum categorizes received
11//!    messages into requests that need responses, notifications that are fire-and-forget,
12//!    or responses that were either routed to pending requests or are unknown.
13//!
14//! 2. **Response Routing**: When the server sends requests to the client, responses
15//!    are automatically routed to the waiting receivers via oneshot channels.
16//!
17//! # Example
18//!
19//! ```
20//! use lsp_server_tokio::{IncomingMessage, Message, Request, Notification, Response};
21//!
22//! // Classify incoming messages
23//! let req = Message::Request(Request::new(1, "textDocument/hover", None));
24//! // After calling route(), you get IncomingMessage::Request(req)
25//!
26//! let notif = Message::Notification(Notification::new("initialized", None));
27//! // After calling route(), you get IncomingMessage::Notification(notif)
28//! ```
29
30use tokio_util::sync::CancellationToken;
31
32use crate::error::{ErrorCode, ResponseError};
33use crate::message::{Notification, Request, Response};
34
35/// Represents a classified incoming message after routing.
36///
37/// When a [`Message`](crate::Message) is received from the transport, it is classified
38/// into one of these variants by the `Connection::route()` method.
39///
40/// # Variants
41///
42/// - [`Request`](IncomingMessage::Request): A request requiring a response. Includes
43///   a [`CancellationToken`] that is triggered on `$/cancelRequest` or shutdown.
44/// - [`Notification`](IncomingMessage::Notification): A fire-and-forget notification.
45///   No response is expected.
46/// - [`ResponseRouted`](IncomingMessage::ResponseRouted): A response that was successfully
47///   delivered to a pending outgoing request's receiver.
48/// - [`ResponseUnknown`](IncomingMessage::ResponseUnknown): A response for which no pending
49///   outgoing request was found. This typically indicates a protocol error or a timed-out request.
50///
51/// # Example
52///
53/// ```
54/// use lsp_server_tokio::{IncomingMessage, Message, Request, Response};
55///
56/// fn handle_message(incoming: IncomingMessage) {
57///     match incoming {
58///         IncomingMessage::Request(req, token) => {
59///             println!("Handle request: {}", req.method);
60///             // Use token for cooperative cancellation
61///             // Send response back
62///         }
63///         IncomingMessage::Notification(notif) => {
64///             println!("Handle notification: {}", notif.method);
65///         }
66///         IncomingMessage::CancelHandled => {
67///             // `$/cancelRequest` was applied automatically
68///         }
69///         IncomingMessage::ResponseRouted => {
70///             // Response was delivered to awaiting task, nothing to do
71///         }
72///         IncomingMessage::ResponseUnknown(resp) => {
73///             println!("Unknown response for id: {:?}", resp.id);
74///             // Log or handle the unexpected response
75///         }
76///         _ => {}
77///     }
78/// }
79/// ```
80#[derive(Debug)]
81#[non_exhaustive]
82pub enum IncomingMessage {
83    /// A request that needs a response.
84    ///
85    /// The server must send a response with the same request ID.
86    /// The included [`CancellationToken`] is triggered when:
87    /// - A `$/cancelRequest` notification is received for this request
88    /// - The connection is shutting down
89    ///
90    /// Use the token for cooperative cancellation of long-running operations.
91    Request(Request, CancellationToken),
92
93    /// A notification (fire-and-forget).
94    ///
95    /// No response is expected or allowed.
96    Notification(Notification),
97
98    /// A `$/cancelRequest` notification that was automatically processed.
99    ///
100    /// The cancellation token for the referenced request (if pending) has already
101    /// been triggered. No further action is needed.
102    CancelHandled,
103
104    /// A response that was successfully delivered to a pending outgoing request.
105    ///
106    /// The response was sent to the oneshot channel registered when the
107    /// outgoing request was created. The awaiting task will receive it.
108    ResponseRouted,
109
110    /// A response for an unknown request ID.
111    ///
112    /// This occurs when:
113    /// - The response ID doesn't match any pending outgoing request
114    /// - The request timed out and was removed before the response arrived
115    /// - The client sent an unsolicited response
116    /// - The response has a null ID (parse error response)
117    ResponseUnknown(Response),
118}
119
120impl IncomingMessage {
121    /// Returns `true` if this message is a routed request.
122    #[must_use]
123    pub fn is_request(&self) -> bool {
124        matches!(self, Self::Request(_, _))
125    }
126
127    /// Returns `true` if this message is a notification.
128    #[must_use]
129    pub fn is_notification(&self) -> bool {
130        matches!(self, Self::Notification(_))
131    }
132
133    /// Returns `true` if this message is an automatically handled cancellation notification.
134    #[must_use]
135    pub fn is_cancel_handled(&self) -> bool {
136        matches!(self, Self::CancelHandled)
137    }
138
139    /// Returns `true` if this message is a response routed to a pending request.
140    #[must_use]
141    pub fn is_response_routed(&self) -> bool {
142        matches!(self, Self::ResponseRouted)
143    }
144
145    /// Returns `true` if this message is a response for an unknown request.
146    #[must_use]
147    pub fn is_response_unknown(&self) -> bool {
148        matches!(self, Self::ResponseUnknown(_))
149    }
150}
151
152/// Creates a `MethodNotFound` error response for an unhandled request.
153///
154/// This helper creates a properly formatted JSON-RPC 2.0 error response
155/// with error code `-32601` (`MethodNotFound`) and a message indicating
156/// which method was not found.
157///
158/// # Arguments
159///
160/// * `request` - The request for which no handler was found
161///
162/// # Returns
163///
164/// A [`Response`] with an error containing the `MethodNotFound` code.
165///
166/// # Example
167///
168/// ```
169/// use lsp_server_tokio::{method_not_found_response, Request, ErrorCode};
170///
171/// let request = Request::new(42, "unknown/method", None);
172/// let response = method_not_found_response(&request);
173///
174/// assert!(response.error().is_some());
175/// let error = response.into_error().unwrap();
176/// assert_eq!(error.code, ErrorCode::MethodNotFound as i32);
177/// assert!(error.message.contains("unknown/method"));
178/// ```
179#[must_use]
180pub fn method_not_found_response(request: &Request) -> Response {
181    let error = ResponseError::new(
182        ErrorCode::MethodNotFound,
183        format!("Method not found: {}", request.method),
184    );
185    Response::err(request.id.clone(), error)
186}
187
188/// Creates a `RequestCancelled` error response for a cancelled request.
189///
190/// This helper creates a properly formatted JSON-RPC 2.0 error response
191/// with error code `-32800` (`RequestCancelled`) and a standard message.
192/// Use this when a request has been cancelled via $/cancelRequest.
193///
194/// # Arguments
195///
196/// * `id` - The request ID of the cancelled request
197///
198/// # Returns
199///
200/// A [`Response`] with an error containing the `RequestCancelled` code.
201///
202/// # Example
203///
204/// ```
205/// use lsp_server_tokio::{cancelled_response, RequestId, ErrorCode};
206///
207/// let id: RequestId = 42.into();
208/// let response = cancelled_response(id);
209///
210/// assert!(response.error().is_some());
211/// let error = response.into_error().unwrap();
212/// assert_eq!(error.code, ErrorCode::RequestCancelled as i32);
213/// ```
214pub fn cancelled_response(id: impl Into<crate::RequestId>) -> Response {
215    let error = ResponseError::new(ErrorCode::RequestCancelled, "Request was cancelled");
216    Response::err(id, error)
217}
218
219#[cfg(test)]
220mod tests {
221    use super::*;
222    use crate::ErrorCode;
223    use serde_json::json;
224
225    #[test]
226    fn method_not_found_response_creates_correct_error() {
227        let request = Request::new(42, "textDocument/unknown", Some(json!({"key": "value"})));
228        let response = method_not_found_response(&request);
229
230        // Verify it's an error response
231        assert!(response.error().is_some());
232        assert!(response.result().is_none());
233
234        // Verify correct ID
235        assert_eq!(response.id, Some(42.into()));
236
237        // Verify error details
238        let error = response.into_error().unwrap();
239        assert_eq!(error.code, ErrorCode::MethodNotFound as i32);
240        assert_eq!(error.code, -32601);
241    }
242
243    #[test]
244    fn method_not_found_response_includes_method_name() {
245        let request = Request::new(1, "custom/myMethod", None);
246        let response = method_not_found_response(&request);
247
248        let error = response.into_error().unwrap();
249        assert!(
250            error.message.contains("custom/myMethod"),
251            "Error message should contain the method name"
252        );
253        assert_eq!(error.message, "Method not found: custom/myMethod");
254    }
255
256    #[test]
257    fn method_not_found_response_with_string_id() {
258        let request = Request::new("request-abc-123", "test/method", None);
259        let response = method_not_found_response(&request);
260
261        assert_eq!(
262            response.id,
263            Some(crate::RequestId::String("request-abc-123".to_string()))
264        );
265    }
266
267    #[test]
268    fn incoming_message_variants_constructible() {
269        use tokio_util::sync::CancellationToken;
270
271        // Test that all variants can be constructed
272        let request = Request::new(1, "test", None);
273        let notification = Notification::new("test", None);
274        let response = Response::ok(1, json!(null));
275        let token = CancellationToken::new();
276
277        let _req = IncomingMessage::Request(request, token);
278        let _notif = IncomingMessage::Notification(notification);
279        let _routed = IncomingMessage::ResponseRouted;
280        let _unknown = IncomingMessage::ResponseUnknown(response);
281    }
282
283    #[test]
284    fn incoming_message_is_debug() {
285        use tokio_util::sync::CancellationToken;
286
287        let request = Request::new(1, "test", None);
288        let token = CancellationToken::new();
289        let incoming = IncomingMessage::Request(request, token);
290        let debug_str = format!("{incoming:?}");
291        assert!(debug_str.contains("Request"));
292    }
293
294    #[test]
295    fn incoming_message_accessors_match_variants() {
296        let request =
297            IncomingMessage::Request(Request::new(1, "test", None), CancellationToken::new());
298        assert!(request.is_request());
299        assert!(!request.is_notification());
300        assert!(!request.is_response_routed());
301        assert!(!request.is_response_unknown());
302        assert!(!request.is_cancel_handled());
303
304        let notification = IncomingMessage::Notification(Notification::new("test", None));
305        assert!(notification.is_notification());
306
307        let routed = IncomingMessage::ResponseRouted;
308        assert!(routed.is_response_routed());
309
310        let unknown = IncomingMessage::ResponseUnknown(Response::ok(1, json!(null)));
311        assert!(unknown.is_response_unknown());
312
313        let cancelled = IncomingMessage::CancelHandled;
314        assert!(cancelled.is_cancel_handled());
315    }
316
317    // ============== cancelled_response Tests ==============
318
319    use super::cancelled_response;
320
321    #[test]
322    fn cancelled_response_creates_correct_error() {
323        let response = cancelled_response(42);
324
325        assert!(response.error().is_some());
326        assert!(response.result().is_none());
327        assert_eq!(response.id, Some(42.into()));
328
329        let error = response.into_error().unwrap();
330        assert_eq!(error.code, ErrorCode::RequestCancelled as i32);
331        assert_eq!(error.code, -32800);
332        assert!(error.message.contains("cancelled"));
333    }
334
335    #[test]
336    fn cancelled_response_with_string_id() {
337        let response = cancelled_response("req-xyz");
338
339        assert_eq!(
340            response.id,
341            Some(crate::RequestId::String("req-xyz".to_string()))
342        );
343        assert!(response.error().is_some());
344    }
345}