Skip to main content

turbomcp_core/
jsonrpc.rs

1//! JSON-RPC 2.0 types - no_std compatible.
2//!
3//! This module provides core JSON-RPC 2.0 types that can be used in `no_std` environments.
4
5use alloc::string::{String, ToString};
6use core::fmt;
7use serde::{Deserialize, Deserializer, Serialize, Serializer};
8use serde_json::Value;
9
10/// JSON-RPC version constant
11pub const JSONRPC_VERSION: &str = "2.0";
12
13/// JSON-RPC version type (always "2.0")
14#[derive(Debug, Clone, PartialEq, Eq, Default)]
15pub struct JsonRpcVersion;
16
17impl Serialize for JsonRpcVersion {
18    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
19    where
20        S: Serializer,
21    {
22        serializer.serialize_str(JSONRPC_VERSION)
23    }
24}
25
26impl<'de> Deserialize<'de> for JsonRpcVersion {
27    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
28    where
29        D: Deserializer<'de>,
30    {
31        let version = String::deserialize(deserializer)?;
32        if version == JSONRPC_VERSION {
33            Ok(JsonRpcVersion)
34        } else {
35            Err(serde::de::Error::custom(alloc::format!(
36                "Invalid JSON-RPC version: expected '{}', got '{}'",
37                JSONRPC_VERSION,
38                version
39            )))
40        }
41    }
42}
43
44/// Request identifier - can be string or number
45#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(untagged)]
47pub enum RequestId {
48    /// String identifier
49    String(String),
50    /// Numeric identifier
51    Number(i64),
52}
53
54impl fmt::Display for RequestId {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        match self {
57            Self::String(s) => write!(f, "{}", s),
58            Self::Number(n) => write!(f, "{}", n),
59        }
60    }
61}
62
63impl From<String> for RequestId {
64    fn from(s: String) -> Self {
65        Self::String(s)
66    }
67}
68
69impl From<&str> for RequestId {
70    fn from(s: &str) -> Self {
71        Self::String(s.to_string())
72    }
73}
74
75impl From<i64> for RequestId {
76    fn from(n: i64) -> Self {
77        Self::Number(n)
78    }
79}
80
81impl From<i32> for RequestId {
82    fn from(n: i32) -> Self {
83        Self::Number(n as i64)
84    }
85}
86
87/// JSON-RPC request message
88#[derive(Debug, Clone, Serialize, Deserialize)]
89pub struct JsonRpcRequest {
90    /// JSON-RPC version
91    pub jsonrpc: JsonRpcVersion,
92    /// Request method name
93    pub method: String,
94    /// Request parameters
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub params: Option<Value>,
97    /// Request identifier
98    pub id: RequestId,
99}
100
101impl JsonRpcRequest {
102    /// Create a new JSON-RPC request
103    #[must_use]
104    pub fn new(method: impl Into<String>, params: Option<Value>, id: impl Into<RequestId>) -> Self {
105        Self {
106            jsonrpc: JsonRpcVersion,
107            method: method.into(),
108            params,
109            id: id.into(),
110        }
111    }
112
113    /// Create a request without parameters
114    #[must_use]
115    pub fn without_params(method: impl Into<String>, id: impl Into<RequestId>) -> Self {
116        Self::new(method, None, id)
117    }
118}
119
120/// JSON-RPC notification (no response expected)
121#[derive(Debug, Clone, Serialize, Deserialize)]
122pub struct JsonRpcNotification {
123    /// JSON-RPC version
124    pub jsonrpc: JsonRpcVersion,
125    /// Notification method name
126    pub method: String,
127    /// Notification parameters
128    #[serde(skip_serializing_if = "Option::is_none")]
129    pub params: Option<Value>,
130}
131
132impl JsonRpcNotification {
133    /// Create a new notification
134    #[must_use]
135    pub fn new(method: impl Into<String>, params: Option<Value>) -> Self {
136        Self {
137            jsonrpc: JsonRpcVersion,
138            method: method.into(),
139            params,
140        }
141    }
142
143    /// Create a notification without parameters
144    #[must_use]
145    pub fn without_params(method: impl Into<String>) -> Self {
146        Self::new(method, None)
147    }
148}
149
150/// JSON-RPC error object
151#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
152pub struct JsonRpcError {
153    /// Error code
154    pub code: i32,
155    /// Error message
156    pub message: String,
157    /// Additional error data
158    #[serde(skip_serializing_if = "Option::is_none")]
159    pub data: Option<Value>,
160}
161
162impl JsonRpcError {
163    /// Create a new error
164    #[must_use]
165    pub fn new(code: i32, message: impl Into<String>) -> Self {
166        Self {
167            code,
168            message: message.into(),
169            data: None,
170        }
171    }
172
173    /// Create an error with additional data
174    #[must_use]
175    pub fn with_data(code: i32, message: impl Into<String>, data: Value) -> Self {
176        Self {
177            code,
178            message: message.into(),
179            data: Some(data),
180        }
181    }
182
183    /// Create a parse error (-32700)
184    #[must_use]
185    pub fn parse_error() -> Self {
186        Self::new(-32700, "Parse error")
187    }
188
189    /// Create an invalid request error (-32600)
190    #[must_use]
191    pub fn invalid_request() -> Self {
192        Self::new(-32600, "Invalid Request")
193    }
194
195    /// Create a method not found error (-32601)
196    #[must_use]
197    pub fn method_not_found(method: &str) -> Self {
198        Self::new(-32601, alloc::format!("Method not found: {}", method))
199    }
200
201    /// Create an invalid params error (-32602)
202    #[must_use]
203    pub fn invalid_params(details: &str) -> Self {
204        Self::new(-32602, alloc::format!("Invalid params: {}", details))
205    }
206
207    /// Create an internal error (-32603)
208    #[must_use]
209    pub fn internal_error(details: &str) -> Self {
210        Self::new(-32603, alloc::format!("Internal error: {}", details))
211    }
212
213    /// Get the error code
214    #[must_use]
215    pub const fn code(&self) -> i32 {
216        self.code
217    }
218
219    /// Check if this is a parse error
220    #[must_use]
221    pub const fn is_parse_error(&self) -> bool {
222        self.code == -32700
223    }
224
225    /// Check if this is an invalid request error
226    #[must_use]
227    pub const fn is_invalid_request(&self) -> bool {
228        self.code == -32600
229    }
230}
231
232impl fmt::Display for JsonRpcError {
233    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
234        write!(f, "[{}] {}", self.code, self.message)
235    }
236}
237
238/// Response ID - handles the case where parse errors have null ID
239#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
240#[serde(transparent)]
241pub struct ResponseId(pub Option<RequestId>);
242
243impl ResponseId {
244    /// Create a response ID from a request ID
245    #[must_use]
246    pub fn from_request(id: RequestId) -> Self {
247        Self(Some(id))
248    }
249
250    /// Create a null response ID (for parse errors)
251    #[must_use]
252    pub fn null() -> Self {
253        Self(None)
254    }
255
256    /// Get the request ID if present
257    #[must_use]
258    pub fn as_request_id(&self) -> Option<&RequestId> {
259        self.0.as_ref()
260    }
261
262    /// Check if this is a null ID
263    #[must_use]
264    pub fn is_null(&self) -> bool {
265        self.0.is_none()
266    }
267}
268
269/// JSON-RPC response payload — enforces JSON-RPC 2.0 §5 mutual exclusion of
270/// `result` and `error` on deserialize. `Serialize` keeps the untagged shape
271/// so the wire format is unchanged.
272#[derive(Debug, Clone, Serialize)]
273#[serde(untagged)]
274pub enum JsonRpcResponsePayload {
275    /// Successful response
276    Success {
277        /// Response result
278        result: Value,
279    },
280    /// Error response
281    Error {
282        /// Response error
283        error: JsonRpcError,
284    },
285}
286
287impl<'de> Deserialize<'de> for JsonRpcResponsePayload {
288    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
289    where
290        D: Deserializer<'de>,
291    {
292        let mut object = serde_json::Map::<String, Value>::deserialize(deserializer)?;
293        let result_present = object.contains_key("result");
294        let error_present = object.contains_key("error");
295
296        match (result_present, error_present) {
297            (true, false) => Ok(Self::Success {
298                result: object.remove("result").unwrap_or(Value::Null),
299            }),
300            (false, true) => {
301                let error_value = object.remove("error").unwrap_or(Value::Null);
302                let error =
303                    JsonRpcError::deserialize(error_value).map_err(serde::de::Error::custom)?;
304                Ok(Self::Error { error })
305            }
306            (true, true) => Err(serde::de::Error::custom(
307                "JSON-RPC response must contain exactly one of `result` or `error`, not both",
308            )),
309            (false, false) => Err(serde::de::Error::custom(
310                "JSON-RPC response must contain exactly one of `result` or `error`",
311            )),
312        }
313    }
314}
315
316/// JSON-RPC response message
317#[derive(Debug, Clone, Serialize, Deserialize)]
318pub struct JsonRpcResponse {
319    /// JSON-RPC version
320    pub jsonrpc: JsonRpcVersion,
321    /// Response payload
322    #[serde(flatten)]
323    pub payload: JsonRpcResponsePayload,
324    /// Response ID
325    pub id: ResponseId,
326}
327
328impl JsonRpcResponse {
329    /// Create a success response
330    #[must_use]
331    pub fn success(result: Value, id: RequestId) -> Self {
332        Self {
333            jsonrpc: JsonRpcVersion,
334            payload: JsonRpcResponsePayload::Success { result },
335            id: ResponseId::from_request(id),
336        }
337    }
338
339    /// Create an error response
340    #[must_use]
341    pub fn error_response(error: JsonRpcError, id: RequestId) -> Self {
342        Self {
343            jsonrpc: JsonRpcVersion,
344            payload: JsonRpcResponsePayload::Error { error },
345            id: ResponseId::from_request(id),
346        }
347    }
348
349    /// Create a parse error response (null ID)
350    #[must_use]
351    pub fn parse_error(message: Option<String>) -> Self {
352        let error = JsonRpcError {
353            code: -32700,
354            message: message.unwrap_or_else(|| "Parse error".to_string()),
355            data: None,
356        };
357        Self {
358            jsonrpc: JsonRpcVersion,
359            payload: JsonRpcResponsePayload::Error { error },
360            id: ResponseId::null(),
361        }
362    }
363
364    /// Check if this is a success response
365    #[must_use]
366    pub fn is_success(&self) -> bool {
367        matches!(self.payload, JsonRpcResponsePayload::Success { .. })
368    }
369
370    /// Check if this is an error response
371    #[must_use]
372    pub fn is_error(&self) -> bool {
373        matches!(self.payload, JsonRpcResponsePayload::Error { .. })
374    }
375
376    /// Get the result if success
377    #[must_use]
378    pub fn result(&self) -> Option<&Value> {
379        match &self.payload {
380            JsonRpcResponsePayload::Success { result } => Some(result),
381            JsonRpcResponsePayload::Error { .. } => None,
382        }
383    }
384
385    /// Get the error if error
386    #[must_use]
387    pub fn error(&self) -> Option<&JsonRpcError> {
388        match &self.payload {
389            JsonRpcResponsePayload::Success { .. } => None,
390            JsonRpcResponsePayload::Error { error } => Some(error),
391        }
392    }
393}
394
395/// Standard JSON-RPC error codes
396#[derive(Debug, Clone, Copy, PartialEq, Eq)]
397pub enum JsonRpcErrorCode {
398    /// Parse error (-32700)
399    ParseError,
400    /// Invalid request (-32600)
401    InvalidRequest,
402    /// Method not found (-32601)
403    MethodNotFound,
404    /// Invalid params (-32602)
405    InvalidParams,
406    /// Internal error (-32603)
407    InternalError,
408    /// Application-defined error
409    ApplicationError(i32),
410}
411
412impl JsonRpcErrorCode {
413    /// Get the numeric code
414    #[must_use]
415    pub const fn code(&self) -> i32 {
416        match self {
417            Self::ParseError => -32700,
418            Self::InvalidRequest => -32600,
419            Self::MethodNotFound => -32601,
420            Self::InvalidParams => -32602,
421            Self::InternalError => -32603,
422            Self::ApplicationError(code) => *code,
423        }
424    }
425
426    /// Get the standard message
427    #[must_use]
428    pub const fn message(&self) -> &'static str {
429        match self {
430            Self::ParseError => "Parse error",
431            Self::InvalidRequest => "Invalid Request",
432            Self::MethodNotFound => "Method not found",
433            Self::InvalidParams => "Invalid params",
434            Self::InternalError => "Internal error",
435            Self::ApplicationError(_) => "Application error",
436        }
437    }
438}
439
440impl fmt::Display for JsonRpcErrorCode {
441    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
442        write!(f, "{} ({})", self.message(), self.code())
443    }
444}
445
446impl From<i32> for JsonRpcErrorCode {
447    fn from(code: i32) -> Self {
448        match code {
449            -32700 => Self::ParseError,
450            -32600 => Self::InvalidRequest,
451            -32601 => Self::MethodNotFound,
452            -32602 => Self::InvalidParams,
453            -32603 => Self::InternalError,
454            other => Self::ApplicationError(other),
455        }
456    }
457}
458
459impl From<JsonRpcErrorCode> for JsonRpcError {
460    fn from(code: JsonRpcErrorCode) -> Self {
461        Self {
462            code: code.code(),
463            message: code.message().to_string(),
464            data: None,
465        }
466    }
467}
468
469// ============================================================================
470// Wire Format Types - for router/transport use
471// ============================================================================
472// These types handle the practical case of deserializing incoming JSON-RPC
473// messages where we don't know upfront if it's a request or notification.
474// Request IDs are validated at this boundary so routers can rely on
475// `id: None` meaning "field absent", not "null or malformed".
476
477/// Incoming JSON-RPC message - can be request or notification.
478///
479/// This is the "wire format" type used by routers to parse incoming messages.
480/// Unlike [`JsonRpcRequest`] which requires an ID, this type can deserialize
481/// both requests (with id) and notifications (without id).
482///
483/// # Example
484///
485/// ```rust
486/// use turbomcp_core::jsonrpc::JsonRpcIncoming;
487///
488/// // Parse a request
489/// let request: JsonRpcIncoming = serde_json::from_str(
490///     r#"{"jsonrpc": "2.0", "id": 1, "method": "ping"}"#
491/// ).unwrap();
492/// assert!(request.is_request());
493///
494/// // Parse a notification
495/// let notification: JsonRpcIncoming = serde_json::from_str(
496///     r#"{"jsonrpc": "2.0", "method": "notifications/initialized"}"#
497/// ).unwrap();
498/// assert!(notification.is_notification());
499/// ```
500#[derive(Debug, Clone)]
501pub struct JsonRpcIncoming {
502    /// JSON-RPC version. Required to be "2.0" per JSON-RPC 2.0 §4.
503    /// Parsed by [`Self::parse`] and checked with [`Self::is_valid_version`]; raw
504    /// deserialize accepts any string for diagnostic purposes (so callers can
505    /// report a 1.0/missing-version error).
506    pub jsonrpc: String,
507    /// Request ID (None for notifications)
508    pub id: Option<Value>,
509    /// Method name
510    pub method: String,
511    /// Method parameters
512    pub params: Option<Value>,
513}
514
515impl<'de> Deserialize<'de> for JsonRpcIncoming {
516    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
517    where
518        D: Deserializer<'de>,
519    {
520        let mut object = serde_json::Map::<String, Value>::deserialize(deserializer)?;
521
522        let jsonrpc = match object.remove("jsonrpc") {
523            Some(Value::String(version)) => version,
524            Some(_) => {
525                return Err(serde::de::Error::custom(
526                    "JSON-RPC request field `jsonrpc` must be a string",
527                ));
528            }
529            None => return Err(serde::de::Error::missing_field("jsonrpc")),
530        };
531
532        let method = match object.remove("method") {
533            Some(Value::String(method)) => method,
534            Some(_) => {
535                return Err(serde::de::Error::custom(
536                    "JSON-RPC request field `method` must be a string",
537                ));
538            }
539            None => return Err(serde::de::Error::missing_field("method")),
540        };
541
542        let id = match object.remove("id") {
543            Some(value) => {
544                Some(validate_incoming_request_id(value).map_err(serde::de::Error::custom)?)
545            }
546            None => None,
547        };
548
549        Ok(Self {
550            jsonrpc,
551            id,
552            method,
553            params: match object.remove("params") {
554                Some(Value::Null) | None => None,
555                Some(params) => Some(params),
556            },
557        })
558    }
559}
560
561fn validate_incoming_request_id(value: Value) -> Result<Value, &'static str> {
562    match &value {
563        Value::String(_) => Ok(value),
564        Value::Number(number) if number.as_i64().is_some() => Ok(value),
565        Value::Null => Err("MCP request `id` must not be null"),
566        Value::Number(_) => Err("MCP request `id` must be an integer"),
567        _ => Err("MCP request `id` must be a string or integer"),
568    }
569}
570
571impl JsonRpcIncoming {
572    /// Check if this is a request (has an ID)
573    #[must_use]
574    pub fn is_request(&self) -> bool {
575        self.id.is_some()
576    }
577
578    /// Check if this is a notification (no ID)
579    #[must_use]
580    pub fn is_notification(&self) -> bool {
581        self.id.is_none()
582    }
583
584    /// Parse from JSON string
585    pub fn parse(input: &str) -> Result<Self, serde_json::Error> {
586        serde_json::from_str(input)
587    }
588
589    /// Validate the JSON-RPC version field is exactly `"2.0"` per JSON-RPC 2.0 §4.
590    /// Callers should invoke this after `parse` to enforce strictness; the raw
591    /// deserialize is intentionally lenient so the parse error path can produce
592    /// a useful `-32600 Invalid Request` response with the offending value.
593    #[must_use]
594    pub fn is_valid_version(&self) -> bool {
595        self.jsonrpc == "2.0"
596    }
597}
598
599/// Outgoing JSON-RPC response - wire format for transport.
600///
601/// This is the "wire format" type used by routers to create responses.
602/// It handles the case where notifications should not receive responses
603/// (represented by having no id, result, or error).
604///
605/// # Example
606///
607/// ```rust
608/// use turbomcp_core::jsonrpc::JsonRpcOutgoing;
609///
610/// // Create a success response
611/// let response = JsonRpcOutgoing::success(
612///     Some(serde_json::json!(1)),
613///     serde_json::json!({"ok": true})
614/// );
615/// assert!(response.should_send());
616///
617/// // Create a notification response (should not be sent)
618/// let no_response = JsonRpcOutgoing::notification_ack();
619/// assert!(!no_response.should_send());
620/// ```
621#[derive(Debug, Clone, Serialize)]
622pub struct JsonRpcOutgoing {
623    /// JSON-RPC version (always "2.0")
624    pub jsonrpc: String,
625    /// Request id, echoed from the originating request.
626    ///
627    /// Per JSON-RPC 2.0 §5.1, error responses for which the id cannot be
628    /// determined (parse error, invalid request) **MUST** contain `id: null`.
629    /// We therefore always serialize this field — `None` becomes `null`.
630    /// Notifications never produce a `JsonRpcOutgoing` that reaches the wire
631    /// (gated by [`Self::should_send`]).
632    pub id: Option<Value>,
633    /// Result (mutually exclusive with error)
634    #[serde(skip_serializing_if = "Option::is_none")]
635    pub result: Option<Value>,
636    /// Error (mutually exclusive with result)
637    #[serde(skip_serializing_if = "Option::is_none")]
638    pub error: Option<JsonRpcError>,
639}
640
641impl JsonRpcOutgoing {
642    /// Create a success response
643    #[must_use]
644    pub fn success(id: Option<Value>, result: Value) -> Self {
645        Self {
646            jsonrpc: "2.0".to_string(),
647            id,
648            result: Some(result),
649            error: None,
650        }
651    }
652
653    /// Create an error response
654    #[must_use]
655    pub fn error(id: Option<Value>, error: impl Into<JsonRpcError>) -> Self {
656        Self {
657            jsonrpc: "2.0".to_string(),
658            id,
659            result: None,
660            error: Some(error.into()),
661        }
662    }
663
664    /// Create a notification acknowledgment (should not be sent over wire)
665    #[must_use]
666    pub fn notification_ack() -> Self {
667        Self {
668            jsonrpc: "2.0".to_string(),
669            id: None,
670            result: None,
671            error: None,
672        }
673    }
674
675    /// Check if this response should be sent over the wire.
676    ///
677    /// Per JSON-RPC 2.0, notifications (requests without id) should not
678    /// receive responses. This method returns false for such cases.
679    #[must_use]
680    pub fn should_send(&self) -> bool {
681        // Normal request/response messages have an id. Parse and invalid-request
682        // errors may not have a readable id but still carry an error payload.
683        self.id.is_some() || self.error.is_some()
684    }
685
686    /// Serialize to JSON string
687    pub fn to_json(&self) -> Result<String, serde_json::Error> {
688        serde_json::to_string(self)
689    }
690}
691
692/// Conversion from McpError to JsonRpcError.
693///
694/// `data` carries whatever the server author attached with
695/// [`McpError::with_data`](crate::error::McpError::with_data) and nothing else:
696/// the remaining context fields (`operation`, `component`, `source_location`)
697/// stay server-side, because they describe TurboMCP internals rather than the
698/// client's request.
699impl From<crate::error::McpError> for JsonRpcError {
700    fn from(err: crate::error::McpError) -> Self {
701        Self {
702            code: err.jsonrpc_code(),
703            message: err.message.clone(),
704            data: err.data().cloned(),
705        }
706    }
707}
708
709#[cfg(test)]
710mod tests {
711    use super::*;
712
713    #[test]
714    fn test_request_creation() {
715        let req = JsonRpcRequest::new("test", None, "id-1");
716        assert_eq!(req.method, "test");
717        assert!(req.params.is_none());
718    }
719
720    #[test]
721    fn test_response_success() {
722        let resp = JsonRpcResponse::success(serde_json::json!({"ok": true}), "id-1".into());
723        assert!(resp.is_success());
724        assert!(!resp.is_error());
725    }
726
727    #[test]
728    fn test_error_codes() {
729        assert_eq!(JsonRpcErrorCode::ParseError.code(), -32700);
730        assert_eq!(
731            JsonRpcErrorCode::from(-32601),
732            JsonRpcErrorCode::MethodNotFound
733        );
734    }
735
736    #[test]
737    fn test_request_id_conversion() {
738        let id1: RequestId = "test".into();
739        assert!(matches!(id1, RequestId::String(_)));
740
741        let id2: RequestId = 42i32.into();
742        assert!(matches!(id2, RequestId::Number(42)));
743    }
744
745    #[test]
746    fn test_incoming_request() {
747        let input = r#"{"jsonrpc": "2.0", "id": 1, "method": "ping"}"#;
748        let incoming = JsonRpcIncoming::parse(input).unwrap();
749        assert!(incoming.is_request());
750        assert!(!incoming.is_notification());
751        assert_eq!(incoming.method, "ping");
752    }
753
754    #[test]
755    fn test_incoming_notification() {
756        let input = r#"{"jsonrpc": "2.0", "method": "notifications/initialized"}"#;
757        let incoming = JsonRpcIncoming::parse(input).unwrap();
758        assert!(!incoming.is_request());
759        assert!(incoming.is_notification());
760    }
761
762    #[test]
763    fn test_incoming_rejects_null_id() {
764        let input = r#"{"jsonrpc": "2.0", "id": null, "method": "ping"}"#;
765        let err = JsonRpcIncoming::parse(input).unwrap_err();
766        assert!(err.to_string().contains("must not be null"));
767    }
768
769    #[test]
770    fn test_incoming_rejects_non_integer_number_id() {
771        let input = r#"{"jsonrpc": "2.0", "id": 1.25, "method": "ping"}"#;
772        let err = JsonRpcIncoming::parse(input).unwrap_err();
773        assert!(err.to_string().contains("must be an integer"));
774    }
775
776    #[test]
777    fn test_outgoing_success() {
778        let response = JsonRpcOutgoing::success(Some(serde_json::json!(1)), serde_json::json!({}));
779        assert!(response.should_send());
780        assert!(response.result.is_some());
781        assert!(response.error.is_none());
782    }
783
784    #[test]
785    fn test_outgoing_success_without_id_is_not_sent() {
786        let response = JsonRpcOutgoing::success(None, serde_json::json!({}));
787        assert!(!response.should_send());
788    }
789
790    #[test]
791    fn test_outgoing_notification_ack() {
792        let response = JsonRpcOutgoing::notification_ack();
793        assert!(!response.should_send());
794    }
795
796    /// JSON-RPC 2.0 §5: a response object MUST contain `result` xor `error`.
797    /// Both-present and neither-present payloads must be rejected.
798    #[test]
799    fn test_response_payload_rejects_both_result_and_error() {
800        let raw = r#"{"jsonrpc":"2.0","id":1,"result":{},"error":{"code":-32603,"message":"x"}}"#;
801        let err = serde_json::from_str::<JsonRpcResponse>(raw)
802            .expect_err("response with both result and error must be rejected");
803        assert!(err.to_string().contains("exactly one of"));
804    }
805
806    #[test]
807    fn test_response_payload_rejects_neither_result_nor_error() {
808        let raw = r#"{"jsonrpc":"2.0","id":1}"#;
809        let err = serde_json::from_str::<JsonRpcResponse>(raw)
810            .expect_err("response with neither result nor error must be rejected");
811        assert!(err.to_string().contains("exactly one of"));
812    }
813
814    #[test]
815    fn test_response_payload_accepts_error_only() {
816        let raw =
817            r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"Method not found"}}"#;
818        let resp: JsonRpcResponse = serde_json::from_str(raw).unwrap();
819        assert!(resp.is_error());
820        assert_eq!(resp.error().unwrap().code, -32601);
821    }
822
823    #[test]
824    fn test_response_payload_accepts_null_result() {
825        let raw = r#"{"jsonrpc":"2.0","id":1,"result":null}"#;
826        let resp: JsonRpcResponse = serde_json::from_str(raw).unwrap();
827        assert!(resp.is_success());
828        assert_eq!(resp.result(), Some(&Value::Null));
829    }
830}