Skip to main content

turul_rpc_jsonrpc/
dispatch.rs

1//! JSON-RPC 2.0 codec — parser, batch handling, response helpers.
2//!
3//! See [ADR-002] in the `turul-rpc` repository for the compliance contract.
4//!
5//! [ADR-002]: https://github.com/aussierobots/turul-rpc/blob/main/docs/adr/002-json-rpc-2-compliance.md
6
7use serde_json::Value;
8
9use turul_rpc_core::error::JsonRpcError;
10use turul_rpc_core::notification::JsonRpcNotification;
11use turul_rpc_core::request::JsonRpcRequest;
12use turul_rpc_core::response::JsonRpcSuccessResponse;
13use turul_rpc_core::types::RequestId;
14
15/// Enum representing the inbound JSON-RPC messages a server parses:
16/// a request or a notification. Responses are produced, not parsed here,
17/// and live in [`turul_rpc_core::response::JsonRpcResponse`].
18#[derive(Debug, Clone)]
19pub enum JsonRpcMessage {
20    Request(JsonRpcRequest),
21    Notification(JsonRpcNotification),
22}
23
24/// Result of parsing and processing a JSON-RPC message.
25#[derive(Debug, Clone)]
26pub enum JsonRpcMessageResult {
27    /// A successful response to a request.
28    Response(JsonRpcSuccessResponse),
29    /// An error response.
30    Error(JsonRpcError),
31    /// No response needed (for notifications).
32    NoResponse,
33}
34
35impl JsonRpcMessageResult {
36    /// Convert to JSON string if there's a response to send.
37    pub fn to_json_string(&self) -> Option<String> {
38        match self {
39            JsonRpcMessageResult::Response(response) => serde_json::to_string(response).ok(),
40            JsonRpcMessageResult::Error(error) => serde_json::to_string(error).ok(),
41            JsonRpcMessageResult::NoResponse => None,
42        }
43    }
44
45    /// Check if this result represents an error.
46    pub fn is_error(&self) -> bool {
47        matches!(self, JsonRpcMessageResult::Error(_))
48    }
49
50    /// Check if this result needs a response.
51    pub fn needs_response(&self) -> bool {
52        !matches!(self, JsonRpcMessageResult::NoResponse)
53    }
54}
55
56/// Parse a JSON string into a single JSON-RPC message.
57///
58/// On parse failure returns `Err(JsonRpcError)` with the appropriate
59/// JSON-RPC 2.0 error code:
60///
61/// - Invalid JSON → `-32700` (Parse error), `id: null`
62/// - Not an object → `-32600` (Invalid Request), `id: null`
63/// - Wrong `jsonrpc` field → `-32600` (Invalid Request), `id: null`
64/// - Object missing required fields → `-32600` (Invalid Request), echoing
65///   the id when one is parseable
66pub fn parse_json_rpc_message(json_str: &str) -> Result<JsonRpcMessage, JsonRpcError> {
67    let value: Value = serde_json::from_str(json_str).map_err(|_| JsonRpcError::parse_error())?;
68    parse_json_rpc_value(value)
69}
70
71/// Parse a `serde_json::Value` into a single JSON-RPC message.
72///
73/// Used by both [`parse_json_rpc_message`] and [`parse_json_rpc_batch`] so
74/// that batch members do not pay a round-trip through string serialization.
75fn parse_json_rpc_value(value: Value) -> Result<JsonRpcMessage, JsonRpcError> {
76    if !value.is_object() {
77        return Err(JsonRpcError::invalid_request(None));
78    }
79
80    let obj = value.as_object().unwrap();
81
82    // Check JSON-RPC version
83    match obj.get("jsonrpc") {
84        Some(version) if version == "2.0" => {}
85        _ => return Err(JsonRpcError::invalid_request(extract_id(obj))),
86    }
87
88    // Check if it has an ID (request) or not (notification)
89    if obj.contains_key("id") {
90        // `"id": null` is accepted per §4.2 (permitted, discouraged) and
91        // parses to RequestId::Null. See ADR-002 (rev 2026-05-24).
92        // Reject fractional numeric ids (no `as_i64()`).
93        if let Some(Value::Number(n)) = obj.get("id")
94            && n.as_i64().is_none()
95        {
96            return Err(JsonRpcError::invalid_request(None));
97        }
98        serde_json::from_value::<JsonRpcRequest>(value.clone())
99            .map(JsonRpcMessage::Request)
100            .map_err(|_| JsonRpcError::invalid_request(extract_id(obj)))
101    } else {
102        serde_json::from_value::<JsonRpcNotification>(value)
103            .map(JsonRpcMessage::Notification)
104            .map_err(|_| JsonRpcError::invalid_request(None))
105    }
106}
107
108pub(crate) fn extract_id(obj: &serde_json::Map<String, Value>) -> Option<RequestId> {
109    obj.get("id").and_then(|v| match v {
110        Value::String(s) => Some(RequestId::String(s.clone())),
111        Value::Number(n) => n.as_i64().map(RequestId::Number),
112        Value::Null => Some(RequestId::Null),
113        _ => None,
114    })
115}
116
117/// Parse multiple JSON-RPC messages from a single JSON string.
118///
119/// **Compatibility shim** for the `turul-mcp-json-rpc-server 0.3.x` API.
120/// New code should use [`crate::batch::parse_json_rpc_batch`] for proper
121/// JSON-RPC 2.0 §6 batch semantics. This function returns a single-element
122/// vec for non-array bodies and an empty vec for an empty array body —
123/// it is **not** spec-correct on its own; the dispatcher must construct
124/// the `Invalid Request` response for empty batches.
125pub fn parse_json_rpc_messages(json_str: &str) -> Vec<Result<JsonRpcMessage, JsonRpcError>> {
126    use crate::batch::{BatchOrSingle, parse_json_rpc_batch};
127    match parse_json_rpc_batch(json_str) {
128        BatchOrSingle::Single(r) => vec![r],
129        BatchOrSingle::Batch(items) => items,
130        BatchOrSingle::EmptyBatch => vec![Err(JsonRpcError::invalid_request(None))],
131    }
132}
133
134/// Parse a single JSON-RPC value as a message. Used by both the single-message
135/// parser and the batch parser to avoid round-trips through string serialization.
136pub(crate) fn parse_value_into_message(value: Value) -> Result<JsonRpcMessage, JsonRpcError> {
137    parse_json_rpc_value(value)
138}
139
140/// Create a simple success response.
141pub fn create_success_response(id: RequestId, result: Value) -> JsonRpcMessageResult {
142    JsonRpcMessageResult::Response(JsonRpcSuccessResponse::success(id, result))
143}
144
145/// Create a simple error response.
146pub fn create_error_response(
147    id: Option<RequestId>,
148    code: i64,
149    message: &str,
150) -> JsonRpcMessageResult {
151    let error_obj = turul_rpc_core::error::JsonRpcErrorObject {
152        code,
153        message: message.to_string(),
154        data: None,
155    };
156    JsonRpcMessageResult::Error(JsonRpcError::new(id, error_obj))
157}
158
159/// Utility functions for working with JSON-RPC messages.
160impl JsonRpcMessage {
161    /// Get the method name.
162    pub fn method(&self) -> &str {
163        match self {
164            JsonRpcMessage::Request(req) => &req.method,
165            JsonRpcMessage::Notification(notif) => &notif.method,
166        }
167    }
168
169    /// Check if this is a request (has ID).
170    pub fn is_request(&self) -> bool {
171        matches!(self, JsonRpcMessage::Request(_))
172    }
173
174    /// Check if this is a notification (no ID).
175    pub fn is_notification(&self) -> bool {
176        matches!(self, JsonRpcMessage::Notification(_))
177    }
178
179    /// Get the request ID if this is a request.
180    pub fn request_id(&self) -> Option<&RequestId> {
181        match self {
182            JsonRpcMessage::Request(req) => Some(&req.id),
183            JsonRpcMessage::Notification(_) => None,
184        }
185    }
186}
187
188#[cfg(test)]
189mod tests {
190    use super::*;
191    use serde_json::json;
192
193    #[test]
194    fn test_parse_valid_request() {
195        let json = r#"{"jsonrpc": "2.0", "method": "test", "id": 1}"#;
196        let message = parse_json_rpc_message(json).unwrap();
197
198        assert!(message.is_request());
199        assert_eq!(message.method(), "test");
200        assert_eq!(message.request_id(), Some(&RequestId::Number(1)));
201    }
202
203    #[test]
204    fn test_parse_valid_notification() {
205        let json = r#"{"jsonrpc": "2.0", "method": "notify"}"#;
206        let message = parse_json_rpc_message(json).unwrap();
207
208        assert!(message.is_notification());
209        assert_eq!(message.method(), "notify");
210        assert_eq!(message.request_id(), None);
211    }
212
213    #[test]
214    fn test_parse_invalid_json() {
215        let json = r#"{"jsonrpc": "2.0", "method": "test""#;
216        let result = parse_json_rpc_message(json);
217
218        assert!(result.is_err());
219        let error = result.unwrap_err();
220        assert_eq!(error.error.code, -32700);
221    }
222
223    #[test]
224    fn test_parse_invalid_version() {
225        let json = r#"{"jsonrpc": "1.0", "method": "test", "id": 1}"#;
226        let result = parse_json_rpc_message(json);
227
228        assert!(result.is_err());
229        let error = result.unwrap_err();
230        assert_eq!(error.error.code, -32600);
231    }
232
233    #[test]
234    fn test_message_result_to_json() {
235        let response = create_success_response(RequestId::Number(1), json!({"result": "success"}));
236
237        let json_str = response.to_json_string().unwrap();
238        assert!(json_str.contains("\"result\""));
239        assert!(json_str.contains("\"jsonrpc\":\"2.0\""));
240    }
241
242    #[test]
243    fn test_message_result_properties() {
244        let success = create_success_response(RequestId::Number(1), json!({}));
245        let error = create_error_response(Some(RequestId::Number(1)), -32601, "Not found");
246        let no_response = JsonRpcMessageResult::NoResponse;
247
248        assert!(!success.is_error());
249        assert!(success.needs_response());
250
251        assert!(error.is_error());
252        assert!(error.needs_response());
253
254        assert!(!no_response.is_error());
255        assert!(!no_response.needs_response());
256    }
257}