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}