Skip to main content

github_copilot_sdk/
jsonrpc.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Instant;
5
6use parking_lot::{Mutex, RwLock};
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
10use tokio::sync::{broadcast, mpsc, oneshot};
11use tokio::task::JoinHandle;
12use tokio_util::sync::CancellationToken;
13use tracing::{Instrument, debug, error, warn};
14
15use crate::{Error, ErrorKind, ProtocolErrorKind};
16
17/// Callback invoked synchronously by the JSON-RPC read loop the instant a
18/// successful response is parsed, before the response is delivered to the
19/// awaiter and before the read loop dispatches the next message. Use this
20/// when client-side state (for example, registering a server-assigned
21/// session id with the router) must be visible to any subsequent
22/// notification on the same connection.
23///
24/// If the callback returns an error, that error is delivered to the
25/// awaiter in place of the response.
26pub(crate) type InlineResponseCallback =
27    Box<dyn FnOnce(&JsonRpcResponse) -> Result<(), Error> + Send + Sync>;
28
29/// Internal pairing of the response delivery channel with an optional
30/// inline callback that the read loop runs synchronously before delivery.
31struct PendingRequest {
32    sender: oneshot::Sender<JsonRpcResponse>,
33    inline_callback: Option<InlineResponseCallback>,
34}
35
36/// A JSON-RPC 2.0 request message.
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct JsonRpcRequest {
40    /// Protocol version (always `"2.0"`).
41    pub jsonrpc: String,
42    /// Request ID for correlating responses.
43    pub id: u64,
44    /// RPC method name.
45    pub method: String,
46    /// Optional method parameters.
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub params: Option<Value>,
49}
50
51/// A JSON-RPC 2.0 response message.
52#[derive(Debug, Clone, Serialize, Deserialize)]
53#[serde(rename_all = "camelCase")]
54pub struct JsonRpcResponse {
55    /// Protocol version (always `"2.0"`).
56    pub jsonrpc: String,
57    /// Request ID this response correlates to.
58    pub id: u64,
59    /// Success payload (mutually exclusive with `error`).
60    #[serde(skip_serializing_if = "Option::is_none")]
61    pub result: Option<Value>,
62    /// Error payload (mutually exclusive with `result`).
63    #[serde(skip_serializing_if = "Option::is_none")]
64    pub error: Option<JsonRpcError>,
65}
66
67/// A JSON-RPC 2.0 error object.
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct JsonRpcError {
70    /// Numeric error code.
71    pub code: i32,
72    /// Human-readable error description.
73    pub message: String,
74    /// Optional structured error data.
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub data: Option<Value>,
77}
78
79/// Standard JSON-RPC 2.0 error codes.
80pub mod error_codes {
81    /// Method not found (-32601).
82    pub const METHOD_NOT_FOUND: i32 = -32601;
83    /// Invalid method parameters (-32602).
84    pub const INVALID_PARAMS: i32 = -32602;
85    /// Internal server error (-32603).
86    #[allow(dead_code, reason = "standard JSON-RPC code, reserved for future use")]
87    pub const INTERNAL_ERROR: i32 = -32603;
88}
89
90/// A JSON-RPC 2.0 notification (no `id`, no response expected).
91#[derive(Debug, Clone, Serialize, Deserialize)]
92#[serde(rename_all = "camelCase")]
93pub struct JsonRpcNotification {
94    /// Protocol version (always `"2.0"`).
95    pub jsonrpc: String,
96    /// Notification method name.
97    pub method: String,
98    /// Optional notification parameters.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub params: Option<Value>,
101}
102
103/// A parsed JSON-RPC 2.0 message — request, response, or notification.
104#[derive(Debug, Clone, Serialize)]
105pub enum JsonRpcMessage {
106    /// An incoming or outgoing request.
107    Request(JsonRpcRequest),
108    /// A response to a previous request.
109    Response(JsonRpcResponse),
110    /// A fire-and-forget notification.
111    Notification(JsonRpcNotification),
112}
113
114/// Custom deserializer that dispatches based on field presence instead of
115/// `#[serde(untagged)]` which tries each variant sequentially (3× parse
116/// attempts for Notification — the hot-path streaming variant).
117///
118/// Dispatch logic:
119/// - has `id` + has `method` → Request
120/// - has `id` + no `method` → Response
121/// - no `id`                → Notification
122impl<'de> Deserialize<'de> for JsonRpcMessage {
123    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
124    where
125        D: serde::Deserializer<'de>,
126    {
127        let value = Value::deserialize(deserializer)?;
128        let obj = value
129            .as_object()
130            .ok_or_else(|| serde::de::Error::custom("expected a JSON object"))?;
131
132        let has_id = obj.contains_key("id");
133        let has_method = obj.contains_key("method");
134
135        if has_id && has_method {
136            JsonRpcRequest::deserialize(value)
137                .map(JsonRpcMessage::Request)
138                .map_err(serde::de::Error::custom)
139        } else if has_id {
140            JsonRpcResponse::deserialize(value)
141                .map(JsonRpcMessage::Response)
142                .map_err(serde::de::Error::custom)
143        } else {
144            JsonRpcNotification::deserialize(value)
145                .map(JsonRpcMessage::Notification)
146                .map_err(serde::de::Error::custom)
147        }
148    }
149}
150
151impl JsonRpcRequest {
152    /// Create a new JSON-RPC request with the given ID, method, and params.
153    pub fn new(id: u64, method: &str, params: Option<Value>) -> Self {
154        Self {
155            jsonrpc: "2.0".to_string(),
156            id,
157            method: method.to_string(),
158            params,
159        }
160    }
161}
162
163impl JsonRpcResponse {
164    /// Returns `true` if this response contains an error.
165    #[allow(dead_code)]
166    pub fn is_error(&self) -> bool {
167        self.error.is_some()
168    }
169}
170
171const CONTENT_LENGTH_HEADER: &str = "Content-Length: ";
172
173/// Rewrites unpaired UTF-16 surrogate escapes to `\uFFFD`.
174///
175/// Returns `None` when the body contains no unpaired surrogate, so valid
176/// frames do not incur a repair allocation.
177fn repair_lone_surrogates(body: &[u8]) -> Option<Vec<u8>> {
178    fn hex_escape_at(body: &[u8], index: usize) -> Option<u16> {
179        let digits = body.get(index + 2..index + 6)?;
180        let text = std::str::from_utf8(digits).ok()?;
181        u16::from_str_radix(text, 16).ok()
182    }
183
184    let mut repaired = None;
185    let mut in_string = false;
186    let mut index = 0;
187
188    while index < body.len() {
189        let byte = body[index];
190
191        if !in_string {
192            in_string = byte == b'"';
193            index += 1;
194            continue;
195        }
196
197        match byte {
198            b'"' => {
199                in_string = false;
200                index += 1;
201            }
202            // Consume non-Unicode escapes whole so an escaped backslash cannot
203            // be mistaken for the start of a surrogate escape.
204            b'\\' if body.get(index + 1) != Some(&b'u') => index += 2,
205            b'\\' => {
206                let Some(unit) = hex_escape_at(body, index) else {
207                    index += 2;
208                    continue;
209                };
210
211                let is_pair = (0xD800..0xDC00).contains(&unit)
212                    && body.get(index + 6) == Some(&b'\\')
213                    && body.get(index + 7) == Some(&b'u')
214                    && hex_escape_at(body, index + 6)
215                        .is_some_and(|low| (0xDC00..0xE000).contains(&low));
216
217                if is_pair {
218                    index += 12;
219                    continue;
220                }
221
222                if (0xD800..0xE000).contains(&unit) {
223                    let output = repaired.get_or_insert_with(|| body.to_vec());
224                    output[index..index + 6].copy_from_slice(br"\ufffd");
225                }
226                index += 6;
227            }
228            _ => index += 1,
229        }
230    }
231
232    repaired
233}
234
235/// One framed JSON-RPC message handed to the writer actor.
236///
237/// `frame` is the fully serialized bytes (header + body); the caller pays
238/// the serde cost synchronously before enqueueing so the actor never sees a
239/// `Result` from JSON encoding. `ack` resolves once the bytes have been
240/// fully written and flushed (or the underlying I/O reports an error). If
241/// the caller drops the `oneshot::Receiver`, the actor still completes the
242/// frame — caller cancellation cannot desync the wire.
243struct WriteCommand {
244    frame: Vec<u8>,
245    ack: oneshot::Sender<Result<(), std::io::Error>>,
246}
247
248/// Low-level JSON-RPC 2.0 client over Content-Length-framed streams.
249///
250/// # Cancel safety
251///
252/// All public methods (`write`, `send_request`) are **cancel-safe**: the
253/// actual bytes hit the wire on a dedicated background actor task, so
254/// dropping the caller's future after `await` returns `Pending` cannot
255/// produce a partial frame on the wire. Frames either land atomically or
256/// the underlying I/O fails. See `cancel-safety review` artifact for the
257/// full RFD-400 reasoning.
258pub struct JsonRpcClient {
259    request_id: AtomicU64,
260    /// Sender side of the writer actor's command queue. Public methods
261    /// pre-serialize their frames and enqueue here; the background actor
262    /// drains the queue and serializes writes onto the underlying
263    /// `AsyncWrite`. Unbounded by design — RFD 400 explicitly permits this
264    /// for cancel-safety, and JSON-RPC frames are small relative to the
265    /// natural request/response back-pressure of the wire.
266    write_tx: mpsc::UnboundedSender<WriteCommand>,
267    pending_requests: Arc<RwLock<HashMap<u64, PendingRequest>>>,
268    notification_tx: broadcast::Sender<JsonRpcNotification>,
269    request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
270    connection_closed: CancellationToken,
271    read_task: Mutex<Option<JoinHandle<()>>>,
272    write_task: Mutex<Option<JoinHandle<()>>>,
273}
274
275impl JsonRpcClient {
276    /// Create a new client from async read/write streams.
277    ///
278    /// Spawns two background tasks: a reader that dispatches incoming
279    /// messages to pending request channels, the notification broadcast,
280    /// or the request-forwarding channel; and a writer actor that owns the
281    /// underlying `AsyncWrite` and serializes frames atomically.
282    pub fn new(
283        writer: impl AsyncWrite + Unpin + Send + 'static,
284        reader: impl AsyncRead + Unpin + Send + 'static,
285        notification_tx: broadcast::Sender<JsonRpcNotification>,
286        request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
287    ) -> Self {
288        let (write_tx, write_rx) = mpsc::unbounded_channel::<WriteCommand>();
289
290        let writer_span = tracing::error_span!("jsonrpc_write_loop");
291        let write_task = tokio::spawn(Self::write_loop(writer, write_rx).instrument(writer_span));
292
293        let client = Self {
294            request_id: AtomicU64::new(1),
295            write_tx,
296            pending_requests: Arc::new(RwLock::new(HashMap::new())),
297            notification_tx,
298            request_tx,
299            connection_closed: CancellationToken::new(),
300            read_task: Mutex::new(None),
301            write_task: Mutex::new(Some(write_task)),
302        };
303
304        let pending_requests = client.pending_requests.clone();
305        let notification_tx_clone = client.notification_tx.clone();
306        let request_tx_clone = client.request_tx.clone();
307        let connection_closed = client.connection_closed.clone();
308        let reader_span = tracing::error_span!("jsonrpc_read_loop");
309
310        let read_task = tokio::spawn(
311            async move {
312                Self::read_loop(
313                    reader,
314                    pending_requests,
315                    notification_tx_clone,
316                    request_tx_clone,
317                )
318                .await;
319                connection_closed.cancel();
320            }
321            .instrument(reader_span),
322        );
323        *client.read_task.lock() = Some(read_task);
324
325        client
326    }
327
328    pub(crate) fn force_close(&self) {
329        self.connection_closed.cancel();
330        if let Some(task) = self.read_task.lock().take() {
331            task.abort();
332        }
333        if let Some(task) = self.write_task.lock().take() {
334            task.abort();
335        }
336        self.pending_requests.write().clear();
337    }
338
339    pub(crate) fn connection_closed_token(&self) -> CancellationToken {
340        self.connection_closed.child_token()
341    }
342
343    /// Writer-actor task. Owns the `AsyncWrite`, drains the command queue,
344    /// and writes each frame atomically (header + body + flush) before
345    /// signaling the ack.
346    ///
347    /// Caller-side cancellation cannot interrupt a write in progress:
348    /// dropping the ack `oneshot::Receiver` does not cancel the in-flight
349    /// I/O. Once `WriteCommand` is enqueued the frame is committed to land
350    /// on the wire (or surface an `io::Error` to the ack receiver if the
351    /// transport is broken).
352    ///
353    /// Exits cleanly when all senders drop (channel closes), flushing any
354    /// final buffered bytes.
355    async fn write_loop(
356        mut writer: impl AsyncWrite + Unpin + Send + 'static,
357        mut rx: mpsc::UnboundedReceiver<WriteCommand>,
358    ) {
359        while let Some(WriteCommand { frame, ack }) = rx.recv().await {
360            let result = async {
361                writer.write_all(&frame).await?;
362                writer.flush().await?;
363                Ok::<_, std::io::Error>(())
364            }
365            .await;
366
367            // Caller may have dropped the ack receiver (e.g. their
368            // `await` was cancelled); that's fine — we still completed
369            // the write, which was the whole point.
370            let _ = ack.send(result);
371        }
372    }
373
374    async fn read_loop(
375        reader: impl AsyncRead + Unpin + Send,
376        pending_requests: Arc<RwLock<HashMap<u64, PendingRequest>>>,
377        notification_tx: broadcast::Sender<JsonRpcNotification>,
378        request_tx: mpsc::UnboundedSender<JsonRpcRequest>,
379    ) {
380        let mut reader = BufReader::new(reader);
381
382        loop {
383            match Self::read_message(&mut reader).await {
384                Ok(Some(message)) => match message {
385                    JsonRpcMessage::Response(mut response) => {
386                        let id = response.id;
387                        let pending = pending_requests.write().remove(&id);
388                        if let Some(PendingRequest {
389                            sender,
390                            inline_callback,
391                        }) = pending
392                        {
393                            // Run the inline callback synchronously on the
394                            // read loop so any state it mutates (e.g.
395                            // registering a server-assigned session id with
396                            // the router) is visible before the loop reads
397                            // and dispatches the next message.
398                            if let Some(cb) = inline_callback
399                                && response.error.is_none()
400                            {
401                                let cb_outcome =
402                                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
403                                        cb(&response)
404                                    }));
405                                match cb_outcome {
406                                    Ok(Ok(())) => {}
407                                    Ok(Err(error)) => {
408                                        response.result = None;
409                                        response.error = Some(JsonRpcError {
410                                            code: -32603,
411                                            message: error.to_string(),
412                                            data: None,
413                                        });
414                                    }
415                                    Err(panic) => {
416                                        let message = panic
417                                            .downcast_ref::<&'static str>()
418                                            .map(|s| (*s).to_string())
419                                            .or_else(|| panic.downcast_ref::<String>().cloned())
420                                            .unwrap_or_else(|| {
421                                                "inline response callback panicked".to_string()
422                                            });
423                                        response.result = None;
424                                        response.error = Some(JsonRpcError {
425                                            code: -32603,
426                                            message,
427                                            data: None,
428                                        });
429                                    }
430                                }
431                            }
432                            if sender.send(response).is_err() {
433                                warn!(request_id = %id, "failed to send response for request");
434                            }
435                        } else {
436                            warn!(request_id = %id, "received response for unknown request id");
437                        }
438                    }
439                    JsonRpcMessage::Notification(notification) => {
440                        let _ = notification_tx.send(notification);
441                    }
442                    JsonRpcMessage::Request(request) => {
443                        if request_tx.send(request).is_err() {
444                            warn!("failed to forward JSON-RPC request, channel closed");
445                        }
446                    }
447                },
448                Ok(None) => {
449                    break;
450                }
451                Err(e) => {
452                    error!(error = %e, "error reading from CLI");
453                    break;
454                }
455            }
456        }
457
458        // Drain in-flight requests so callers observe cancellation
459        // instead of hanging on a oneshot receiver.
460        let mut pending = pending_requests.write();
461        if !pending.is_empty() {
462            warn!(
463                count = pending.len(),
464                "draining pending requests after read loop exit"
465            );
466            pending.clear();
467        }
468    }
469
470    async fn read_message(
471        reader: &mut BufReader<impl AsyncRead + Unpin>,
472    ) -> Result<Option<JsonRpcMessage>, Error> {
473        let mut line = String::new();
474        let mut content_length = None;
475
476        loop {
477            line.clear();
478            if reader.read_line(&mut line).await? == 0 {
479                return Ok(None);
480            }
481
482            let trimmed = line.trim();
483            if trimmed.is_empty() {
484                break;
485            }
486
487            if let Some(value) = trimmed.strip_prefix(CONTENT_LENGTH_HEADER) {
488                content_length = Some(value.trim().parse::<usize>().map_err(|_| {
489                    Error::from(ErrorKind::Protocol(
490                        ProtocolErrorKind::InvalidContentLength(value.trim().to_string()),
491                    ))
492                })?);
493            }
494        }
495
496        let Some(length) = content_length else {
497            return Err(ErrorKind::Protocol(ProtocolErrorKind::MissingContentLength).into());
498        };
499
500        let mut body = vec![0u8; length];
501        reader.read_exact(&mut body).await?;
502
503        match serde_json::from_slice::<JsonRpcMessage>(&body) {
504            Ok(message) => Ok(Some(message)),
505            Err(error) => {
506                // Dropping an undecodable frame could leave its pending
507                // request waiting forever because this layer has no timeout.
508                match repair_lone_surrogates(&body)
509                    .and_then(|repaired| serde_json::from_slice::<JsonRpcMessage>(&repaired).ok())
510                {
511                    Some(message) => {
512                        warn!(
513                            error = %error,
514                            length,
515                            "recovered JSON-RPC frame containing unpaired UTF-16 surrogates"
516                        );
517                        Ok(Some(message))
518                    }
519                    None => Err(error.into()),
520                }
521            }
522        }
523    }
524
525    /// Send a JSON-RPC request and wait for the matching response.
526    ///
527    /// # Cancel safety
528    ///
529    /// **Cancel-safe.** The frame is committed to the wire via the writer
530    /// actor before this future yields; cancelling the await drops the
531    /// response oneshot but does not desync the transport. The pending-
532    /// requests map is cleaned up automatically (the `PendingGuard` drop
533    /// removes the entry, and the read loop's response handling tolerates
534    /// a missing entry).
535    #[allow(dead_code, reason = "public API exported via crate::JsonRpcClient")]
536    pub async fn send_request(
537        &self,
538        method: &str,
539        params: Option<serde_json::Value>,
540    ) -> Result<JsonRpcResponse, Error> {
541        self.send_request_with_inline_callback(method, params, None)
542            .await
543    }
544
545    /// Send a JSON-RPC request whose response is observed synchronously
546    /// by the read loop *before* it is delivered to the awaiter.
547    ///
548    /// The optional `inline_callback` runs on the JSON-RPC read task the
549    /// instant a successful response is parsed, and before the read loop
550    /// dispatches the next message. This is the only way to perform
551    /// client-side bookkeeping (for example, registering a server-
552    /// assigned session id with the router) that must be visible to any
553    /// notification or request that the server may emit on the same
554    /// connection immediately after the response.
555    ///
556    /// If the callback returns an error or panics, that error is
557    /// surfaced to the awaiter in place of the original response (the
558    /// response payload is discarded and an internal-error JSON-RPC
559    /// error is delivered instead). The error is never propagated back
560    /// to the server and does not crash the read loop.
561    pub(crate) async fn send_request_with_inline_callback(
562        &self,
563        method: &str,
564        params: Option<serde_json::Value>,
565        inline_callback: Option<InlineResponseCallback>,
566    ) -> Result<JsonRpcResponse, Error> {
567        let request_start = Instant::now();
568        let id = self.request_id.fetch_add(1, Ordering::SeqCst);
569        let request = JsonRpcRequest::new(id, method, params);
570
571        let (tx, rx) = oneshot::channel();
572        self.pending_requests.write().insert(
573            id,
574            PendingRequest {
575                sender: tx,
576                inline_callback,
577            },
578        );
579
580        // RAII guard that removes the pending entry if this future is
581        // dropped before the response arrives. Disarmed below before the
582        // success return so the read loop owns the cleanup on the happy
583        // path.
584        let mut guard = PendingGuard {
585            map: &self.pending_requests,
586            id,
587            armed: true,
588        };
589
590        // The PendingGuard's drop removes the entry on every error path
591        // and on cancellation; disarmed below before the success return so
592        // the read loop owns the cleanup on the happy path.
593        if let Err(error) = self.write(&request).await {
594            warn!(
595                elapsed_ms = request_start.elapsed().as_millis(),
596                method = %method,
597                request_id = id,
598                status = "failed",
599                error = %error,
600                "JsonRpcClient::send_request JSON-RPC request finished"
601            );
602            return Err(error);
603        }
604
605        let response = match rx.await {
606            Ok(response) => response,
607            Err(_) => {
608                let error = ErrorKind::Protocol(ProtocolErrorKind::RequestCancelled).into();
609                warn!(
610                    elapsed_ms = request_start.elapsed().as_millis(),
611                    method = %method,
612                    request_id = id,
613                    status = "failed",
614                    error = %error,
615                    "JsonRpcClient::send_request JSON-RPC request finished"
616                );
617                return Err(error);
618            }
619        };
620        guard.disarm();
621        if let Some(error) = &response.error {
622            warn!(
623                elapsed_ms = request_start.elapsed().as_millis(),
624                method = %method,
625                request_id = id,
626                status = "failed",
627                code = error.code,
628                error = %error.message,
629                "JsonRpcClient::send_request JSON-RPC request finished"
630            );
631        } else {
632            debug!(
633                elapsed_ms = request_start.elapsed().as_millis(),
634                method = %method,
635                request_id = id,
636                status = "succeeded",
637                "JsonRpcClient::send_request JSON-RPC request finished"
638            );
639        }
640        Ok(response)
641    }
642
643    /// Write a Content-Length-framed JSON-RPC message to the transport.
644    ///
645    /// # Cancel safety
646    ///
647    /// **Cancel-safe.** Pre-serializes the body, enqueues it on the writer
648    /// actor's command channel, and awaits an ack. Caller cancellation
649    /// drops the ack receiver; the actor still completes the frame and
650    /// flushes. A partial frame can never appear on the wire.
651    pub async fn write<T: serde::Serialize>(&self, message: &T) -> Result<(), Error> {
652        let body = serde_json::to_vec(message)?;
653        let mut frame = Vec::with_capacity(CONTENT_LENGTH_HEADER.len() + 16 + body.len() + 4);
654        frame.extend_from_slice(CONTENT_LENGTH_HEADER.as_bytes());
655        frame.extend_from_slice(body.len().to_string().as_bytes());
656        frame.extend_from_slice(b"\r\n\r\n");
657        frame.extend_from_slice(&body);
658
659        let (ack_tx, ack_rx) = oneshot::channel();
660        self.write_tx
661            .send(WriteCommand { frame, ack: ack_tx })
662            .map_err(|_| {
663                Error::from(std::io::Error::new(
664                    std::io::ErrorKind::BrokenPipe,
665                    "writer actor has shut down",
666                ))
667            })?;
668
669        match ack_rx.await {
670            Ok(Ok(())) => Ok(()),
671            Ok(Err(e)) => Err(Error::from(e)),
672            Err(_) => Err(Error::from(std::io::Error::new(
673                std::io::ErrorKind::BrokenPipe,
674                "writer actor dropped ack without responding",
675            ))),
676        }
677    }
678}
679
680/// RAII guard that removes a pending-request entry from the map if the
681/// owning future is dropped before the response arrives. Disarmed on the
682/// happy path so the read loop's response handling owns the cleanup.
683struct PendingGuard<'a> {
684    map: &'a RwLock<HashMap<u64, PendingRequest>>,
685    id: u64,
686    armed: bool,
687}
688
689impl PendingGuard<'_> {
690    fn disarm(&mut self) {
691        self.armed = false;
692    }
693}
694
695impl Drop for PendingGuard<'_> {
696    fn drop(&mut self) {
697        if self.armed {
698            self.map.write().remove(&self.id);
699        }
700    }
701}
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706
707    #[test]
708    fn deserialize_notification() {
709        let json = r#"{"jsonrpc":"2.0","method":"session.event","params":{"id":"e1"}}"#;
710        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
711        assert!(matches!(msg, JsonRpcMessage::Notification(n) if n.method == "session.event"));
712    }
713
714    #[test]
715    fn deserialize_request() {
716        let json =
717            r#"{"jsonrpc":"2.0","id":5,"method":"permission.request","params":{"kind":"shell"}}"#;
718        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
719        assert!(
720            matches!(msg, JsonRpcMessage::Request(r) if r.id == 5 && r.method == "permission.request")
721        );
722    }
723
724    #[test]
725    fn deserialize_response_with_result() {
726        let json = r#"{"jsonrpc":"2.0","id":3,"result":{"ok":true}}"#;
727        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
728        assert!(matches!(msg, JsonRpcMessage::Response(r) if r.id == 3 && !r.is_error()));
729    }
730
731    #[test]
732    fn deserialize_error_response() {
733        let json =
734            r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32600,"message":"Invalid Request"}}"#;
735        let msg: JsonRpcMessage = serde_json::from_str(json).unwrap();
736        match msg {
737            JsonRpcMessage::Response(r) => {
738                assert!(r.is_error());
739                let err = r.error.unwrap();
740                assert_eq!(err.code, -32600);
741                assert_eq!(err.message, "Invalid Request");
742            }
743            other => panic!("expected Response, got {other:?}"),
744        }
745    }
746
747    #[test]
748    fn deserialize_rejects_non_object() {
749        let result = serde_json::from_str::<JsonRpcMessage>(r#""not an object""#);
750        assert!(result.is_err());
751    }
752
753    #[test]
754    fn request_new_sets_version() {
755        let req = JsonRpcRequest::new(42, "test.method", None);
756        assert_eq!(req.jsonrpc, "2.0");
757        assert_eq!(req.id, 42);
758        assert_eq!(req.method, "test.method");
759        assert!(req.params.is_none());
760    }
761
762    #[test]
763    fn request_serializes_camel_case() {
764        let req = JsonRpcRequest::new(1, "ping", Some(serde_json::json!({})));
765        let json = serde_json::to_string(&req).unwrap();
766        assert!(json.contains(r#""jsonrpc":"2.0""#));
767        assert!(json.contains(r#""id":1"#));
768        assert!(json.contains(r#""method":"ping""#));
769    }
770
771    #[test]
772    fn notification_without_params_omits_field() {
773        let n = JsonRpcNotification {
774            jsonrpc: "2.0".into(),
775            method: "ping".into(),
776            params: None,
777        };
778        let json = serde_json::to_string(&n).unwrap();
779        assert!(!json.contains("params"));
780    }
781
782    #[test]
783    fn response_without_error_omits_field() {
784        let r = JsonRpcResponse {
785            jsonrpc: "2.0".into(),
786            id: 1,
787            result: Some(serde_json::json!(true)),
788            error: None,
789        };
790        let json = serde_json::to_string(&r).unwrap();
791        assert!(!json.contains("error"));
792    }
793}