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}