Skip to main content

turul_rpc_jsonrpc/
frame.rs

1//! The bidirectional wire-message union.
2//!
3//! [`JsonRpcWireMessage`] is the JSON-RPC 2.0 / MCP schema `JSONRPCMessage`:
4//! any single message a peer reads off the wire without knowing direction.
5//! It is the superset of the inbound [`JsonRpcMessage`](crate::JsonRpcMessage)
6//! (request | notification) that the server dispatcher consumes — it adds the
7//! `Response` arm a client or bidirectional peer needs.
8//!
9//! This module is intentionally kept out of the `dispatch` path so the
10//! `turul-mcp-json-rpc-server` (0.3.x) shim does not surface it. See ADR-003
11//! and ADR-004.
12
13use serde::Serialize;
14use serde_json::Value;
15
16use turul_rpc_core::error::JsonRpcError;
17use turul_rpc_core::notification::JsonRpcNotification;
18use turul_rpc_core::request::JsonRpcRequest;
19use turul_rpc_core::response::JsonRpcResponse;
20
21use crate::dispatch::{JsonRpcMessage, extract_id, parse_value_into_message};
22
23/// Any single JSON-RPC 2.0 message read off the wire, in either direction.
24///
25/// The `Response` arm carries a [`JsonRpcResponse`], which is itself the §5
26/// `Success | Error` union — so one variant covers both response kinds.
27///
28/// Serialization is untagged: a frame serializes to the bare message object,
29/// not an externally-tagged wrapper. Parse incoming bytes with
30/// [`parse_json_rpc_wire_message`]; it validates structure rather than relying
31/// on `#[serde(untagged)]` deserialization, which would silently misclassify
32/// a malformed-id request as a notification.
33#[derive(Debug, Clone, Serialize)]
34#[serde(untagged)]
35pub enum JsonRpcWireMessage {
36    /// A request: has a `method` and an `id`, expects a response.
37    Request(JsonRpcRequest),
38    /// A notification: has a `method`, no `id`, expects no response.
39    Notification(JsonRpcNotification),
40    /// A response to a previously-sent request (success or error).
41    Response(JsonRpcResponse),
42}
43
44impl From<JsonRpcMessage> for JsonRpcWireMessage {
45    fn from(message: JsonRpcMessage) -> Self {
46        match message {
47            JsonRpcMessage::Request(request) => JsonRpcWireMessage::Request(request),
48            JsonRpcMessage::Notification(notification) => {
49                JsonRpcWireMessage::Notification(notification)
50            }
51        }
52    }
53}
54
55/// Parse a JSON string into a [`JsonRpcWireMessage`].
56///
57/// On failure returns `Err(JsonRpcError)` with the spec error code:
58/// `-32700` (Parse error) for invalid JSON, `-32600` (Invalid Request) for a
59/// non-object body, a wrong `jsonrpc` version, a fractional numeric id, or a
60/// body that is neither a valid request/notification nor a response.
61pub fn parse_json_rpc_wire_message(json_str: &str) -> Result<JsonRpcWireMessage, JsonRpcError> {
62    let value: Value = serde_json::from_str(json_str).map_err(|_| JsonRpcError::parse_error())?;
63    parse_wire_value(value)
64}
65
66fn parse_wire_value(value: Value) -> Result<JsonRpcWireMessage, JsonRpcError> {
67    let Some(obj) = value.as_object() else {
68        return Err(JsonRpcError::invalid_request(None));
69    };
70
71    // A `method` means request-or-notification. Delegate to the inbound
72    // parser, which already validates the version and rejects fractional ids.
73    if obj.contains_key("method") {
74        return parse_value_into_message(value).map(Into::into);
75    }
76
77    // No `method` → a response. Validate the version, then parse the §5 union.
78    match obj.get("jsonrpc") {
79        Some(version) if version == "2.0" => {}
80        _ => return Err(JsonRpcError::invalid_request(extract_id(obj))),
81    }
82    let id = extract_id(obj);
83    serde_json::from_value::<JsonRpcResponse>(value)
84        .map(JsonRpcWireMessage::Response)
85        .map_err(|_| JsonRpcError::invalid_request(id))
86}