use std::borrow::Cow;
use serde::Deserialize;
use serde_json::value::RawValue;
const JSONRPC_VERSION: &str = "2.0";
pub type Method<'a> = Cow<'a, str>;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum WireId<'a> {
Number(i64),
Text(Cow<'a, str>),
}
impl<'a> WireId<'a> {
#[must_use]
pub fn to_owned(&self) -> WireId<'static> {
match self {
Self::Number(value) => WireId::Number(*value),
Self::Text(text) => WireId::Text(Cow::Owned(text.as_ref().to_owned())),
}
}
pub fn from_raw(value: &'a RawValue) -> Result<Self, WireIdError> {
let raw = value.get();
if raw.starts_with('"') {
return Ok(match serde_json::from_str::<&'a str>(raw) {
Ok(borrowed) => Self::Text(Cow::Borrowed(borrowed)),
Err(_) => match serde_json::from_str::<String>(raw) {
Ok(owned) => Self::Text(Cow::Owned(owned)),
Err(_) => return Err(WireIdError::NotAnId),
},
});
}
match serde_json::from_str::<i64>(raw) {
Ok(number) => Ok(Self::Number(number)),
Err(_) if serde_json::from_str::<f64>(raw).is_ok() => Err(WireIdError::NotAnInteger),
Err(_) => Err(WireIdError::NotAnId),
}
}
#[must_use]
pub const fn text(value: &'a str) -> Self {
Self::Text(Cow::Borrowed(value))
}
#[must_use]
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(value) => Some(value.as_ref()),
Self::Number(_) => None,
}
}
#[must_use]
pub const fn as_number(&self) -> Option<i64> {
match *self {
Self::Number(value) => Some(value),
Self::Text(_) => None,
}
}
#[must_use]
pub fn to_json(&self) -> String {
match self {
Self::Number(value) => value.to_string(),
Self::Text(value) => serde_json::to_string(value.as_ref()).unwrap_or_default(),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum WireIdError {
#[error("a JSON-RPC id must be a number or a string")]
NotAnId,
#[error("a JSON-RPC id must not contain fractional parts")]
NotAnInteger,
}
#[derive(Debug, Clone)]
pub struct WireError<'a> {
pub code: i64,
pub message: &'a str,
pub data: Option<&'a RawValue>,
}
#[derive(Debug, Clone)]
pub enum Outcome<'a> {
Result(&'a RawValue),
Error(WireError<'a>),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum MessageKind {
Request,
Notification,
Result,
Error,
}
impl MessageKind {
#[must_use]
pub const fn expects_reply(self) -> bool {
matches!(self, Self::Request)
}
}
#[derive(Debug, Clone)]
pub struct Envelope<'a> {
pub(crate) kind: MessageKind,
pub(crate) id: Option<WireId<'a>>,
pub(crate) method: Option<Method<'a>>,
pub(crate) params: Option<&'a RawValue>,
pub(crate) outcome: Option<Outcome<'a>>,
}
impl<'a> Envelope<'a> {
#[must_use]
pub const fn kind(&self) -> MessageKind {
self.kind
}
#[must_use]
pub const fn id(&self) -> Option<&WireId<'a>> {
self.id.as_ref()
}
#[must_use]
pub const fn method(&self) -> Option<&Method<'a>> {
self.method.as_ref()
}
#[must_use]
pub fn method_str(&self) -> Option<&str> {
self.method.as_deref()
}
#[must_use]
pub const fn params(&self) -> Option<&'a RawValue> {
self.params
}
#[must_use]
pub const fn outcome(&self) -> Option<&Outcome<'a>> {
self.outcome.as_ref()
}
#[must_use]
pub const fn expects_reply(&self) -> bool {
self.kind.expects_reply()
}
#[must_use]
pub fn routeable(&self) -> Option<(&Method<'a>, Option<&WireId<'a>>)> {
match self.kind {
MessageKind::Request | MessageKind::Notification => Some((
self.method.as_ref().unwrap_or(&Method::Borrowed("")),
self.id.as_ref(),
)),
MessageKind::Result | MessageKind::Error => None,
}
}
}
#[derive(Debug, Clone)]
pub enum Frame<'a> {
Single(Envelope<'a>),
Batch(Vec<Envelope<'a>>),
}
impl<'a> Frame<'a> {
#[must_use]
pub fn envelopes(&self) -> &[Envelope<'a>] {
match self {
Self::Single(envelope) => std::slice::from_ref(envelope),
Self::Batch(envelopes) => envelopes,
}
}
#[must_use]
pub fn into_envelopes(self) -> Vec<Envelope<'a>> {
match self {
Self::Single(envelope) => vec![envelope],
Self::Batch(envelopes) => envelopes,
}
}
#[must_use]
pub const fn is_batch(&self) -> bool {
matches!(self, Self::Batch(_))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum DecodeError {
#[error("the frame is not valid JSON")]
Json,
#[error("{0}")]
InvalidRequest(&'static str),
#[error("a batch must contain at least one message")]
EmptyBatch,
}
impl DecodeError {
#[must_use]
pub const fn code(self) -> crate::domain::ir::IrErrorCode {
match self {
Self::Json => crate::domain::ir::IrErrorCode::ParseError,
Self::InvalidRequest(_) | Self::EmptyBatch => {
crate::domain::ir::IrErrorCode::InvalidRequest
}
}
}
}
#[derive(Deserialize)]
struct WireMessage<'a> {
jsonrpc: &'a str,
#[serde(borrow, default, deserialize_with = "present_member")]
id: Option<&'a RawValue>,
#[serde(borrow, default)]
method: Option<&'a RawValue>,
#[serde(borrow, default)]
params: Option<&'a RawValue>,
#[serde(borrow, default, deserialize_with = "present_member")]
result: Option<&'a RawValue>,
#[serde(borrow, default)]
error: Option<WireErrorRaw<'a>>,
}
fn method_name<'a>(raw: &'a RawValue) -> Method<'a> {
let text = raw.get();
if let Ok(borrowed) = serde_json::from_str::<&'a str>(text) {
return Method::Borrowed(borrowed);
}
serde_json::from_str::<String>(text).map_or(Method::Borrowed(""), Method::Owned)
}
fn present_member<'de, 'a, D>(deserializer: D) -> Result<Option<&'a RawValue>, D::Error>
where
D: serde::Deserializer<'de>,
'de: 'a,
{
Ok(Some(serde::Deserialize::deserialize(deserializer)?))
}
#[derive(Deserialize)]
struct WireErrorRaw<'a> {
code: i64,
#[serde(borrow)]
message: &'a str,
#[serde(borrow)]
data: Option<&'a RawValue>,
}
fn first_non_whitespace(input: &[u8]) -> Option<u8> {
input
.iter()
.find(|byte| !byte.is_ascii_whitespace())
.copied()
}
impl<'a> From<WireMessage<'a>> for Envelope<'a> {
fn from(wire: WireMessage<'a>) -> Self {
let id = wire.id.and_then(|raw| WireId::from_raw(raw).ok());
let outcome = match (wire.result, wire.error) {
(Some(result), None) => Some(Outcome::Result(result)),
(None, Some(error)) => Some(Outcome::Error(WireError {
code: error.code,
message: error.message,
data: error.data,
})),
_ => None,
};
let kind = classify(wire.method.is_some(), wire.id.is_some(), outcome.as_ref());
Self {
kind,
id,
method: wire.method.map(method_name),
params: wire.params,
outcome,
}
}
}
const fn classify(has_method: bool, has_id: bool, outcome: Option<&Outcome<'_>>) -> MessageKind {
match (has_method, has_id) {
(true, true) => MessageKind::Request,
(true, false) => MessageKind::Notification,
(false, _) => match outcome {
Some(Outcome::Error(_)) => MessageKind::Error,
Some(Outcome::Result(_)) | None => MessageKind::Result,
},
}
}
pub fn decode(input: &[u8]) -> Result<Frame<'_>, DecodeError> {
match first_non_whitespace(input) {
Some(b'[') => {
let messages: Vec<WireMessage<'_>> =
serde_json::from_slice(input).map_err(|error| map_json_error(&error))?;
if messages.is_empty() {
return Err(DecodeError::EmptyBatch);
}
let mut out = Vec::with_capacity(messages.len());
for message in &messages {
validate(message)?;
out.push(Envelope::from(clone_shape(message)));
}
Ok(Frame::Batch(out))
}
Some(b'{') => {
let message: WireMessage<'_> =
serde_json::from_slice(input).map_err(|error| map_json_error(&error))?;
validate(&message)?;
Ok(Frame::Single(message.into()))
}
Some(_) => Err(DecodeError::InvalidRequest(
"the frame is not an object or an array",
)),
None => Err(DecodeError::Json),
}
}
fn map_json_error(error: &serde_json::Error) -> DecodeError {
if error.is_syntax() || error.is_eof() {
DecodeError::Json
} else {
DecodeError::InvalidRequest("the frame is not a valid JSON-RPC message")
}
}
fn clone_shape<'a>(message: &WireMessage<'a>) -> WireMessage<'a> {
let WireMessage {
jsonrpc,
id,
method,
params,
result,
error,
} = message;
WireMessage {
jsonrpc,
id: *id,
method: *method,
params: *params,
result: *result,
error: error.as_ref().map(|error| WireErrorRaw {
code: error.code,
message: error.message,
data: error.data,
}),
}
}
fn validate(message: &WireMessage<'_>) -> Result<(), DecodeError> {
if message.jsonrpc != JSONRPC_VERSION {
return Err(DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\""));
}
let has_reply = message.result.is_some() || message.error.is_some();
match (message.method, has_reply) {
(Some(_), true) => {
return Err(DecodeError::InvalidRequest(
"a message cannot have both a method and a reply",
));
}
(None, false) => {
return Err(DecodeError::InvalidRequest(
"a message needs a method or a reply",
));
}
_ => {}
}
if let Some(raw_id) = message.id
&& let Err(error) = WireId::from_raw(raw_id)
{
return Err(DecodeError::InvalidRequest(match error {
WireIdError::NotAnId => "an id must be a number or a string",
WireIdError::NotAnInteger => "a numeric id must not have a fractional part",
}));
}
Ok(())
}
#[cfg(test)]
mod tests {
#![expect(
clippy::panic,
clippy::unwrap_used,
reason = "tests assert on fallible calls and panic with the failing precondition; propagating errors would replace a clear failure message with noise"
)]
use crate::domain::RequestId;
use crate::domain::ir::IrErrorCode;
use crate::domain::jsonrpc::{
DecodeError, Envelope, Frame, MessageKind, Method, Outcome, WireId, WireIdError, decode,
};
use assert_matches::assert_matches;
type TestError = Box<dyn std::error::Error>;
fn single(input: &[u8]) -> Result<Envelope<'_>, TestError> {
let frame = decode(input)?;
match frame {
Frame::Single(envelope) => Ok(envelope),
Frame::Batch(_) => Err("expected a single message, got a batch".into()),
}
}
fn first(text: &[u8]) -> Result<Envelope<'_>, TestError> {
single(text)
}
#[test]
fn a_request_carries_a_method_and_an_id() -> Result<(), TestError> {
let envelope = first(r#"{"jsonrpc":"2.0","id":1,"method":"tools/call"}"#.as_bytes())?;
assert_eq!(envelope.kind(), MessageKind::Request);
assert_eq!(envelope.method_str(), Some("tools/call"));
assert_eq!(envelope.id(), Some(&WireId::Number(1)));
assert!(envelope.expects_reply());
assert!(envelope.outcome().is_none());
Ok(())
}
#[test]
fn a_notification_has_no_id_and_expects_nothing() -> Result<(), TestError> {
let envelope =
first(r#"{"jsonrpc":"2.0","method":"notifications/initialized"}"#.as_bytes())?;
assert_eq!(envelope.kind(), MessageKind::Notification);
assert_eq!(envelope.id(), None);
assert!(!envelope.expects_reply());
Ok(())
}
#[test]
fn a_result_and_an_error_are_both_replies() -> Result<(), TestError> {
let result = first(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#.as_bytes())?;
assert_eq!(result.kind(), MessageKind::Result);
assert!(!result.expects_reply());
assert_matches!(result.outcome(), Some(Outcome::Result(_)));
let error = first(
r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"nope"}}"#.as_bytes(),
)?;
assert_eq!(error.kind(), MessageKind::Error);
assert_matches!(
error.outcome(),
Some(Outcome::Error(wire)) if wire.code == -32601 && wire.message == "nope"
);
Ok(())
}
#[test]
fn malformed_json_is_parse_error() {
let error = decode(b"{\"jsonrpc\":")
.map(|frame| format!("{frame:?}"))
.unwrap_err();
assert_eq!(error, DecodeError::Json);
assert_eq!(error.code(), IrErrorCode::ParseError);
assert_eq!(error.code().code(), -32700);
}
#[test]
fn a_wrong_jsonrpc_version_is_invalid_request() {
let error = first(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#.as_bytes()).unwrap_err();
assert_eq!(
error.downcast_ref::<DecodeError>(),
Some(&DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\"")),
);
assert_eq!(
decode(r#"{"jsonrpc":"1.0","id":1,"method":"ping"}"#.as_bytes())
.unwrap_err()
.code(),
IrErrorCode::InvalidRequest,
);
assert_eq!(
IrErrorCode::InvalidRequest.code(),
-32600,
"the code a peer switches on must be the spec's, not merely the variant's name"
);
}
#[test]
fn a_missing_id_on_a_request_is_invalid_request() {
let frame = decode(br#"{"jsonrpc":"2.0","method":"ping"}"#);
let Ok(Frame::Single(envelope)) = frame else {
panic!("a method without an id is still a well-formed message");
};
assert_eq!(envelope.kind(), MessageKind::Notification);
assert_eq!(envelope.id(), None);
}
#[test]
fn a_method_beside_a_result_is_invalid_request() {
assert_eq!(
decode(br#"{"jsonrpc":"2.0","id":1,"method":"ping","result":{}}"#).unwrap_err(),
DecodeError::InvalidRequest("a message cannot have both a method and a reply"),
);
}
#[test]
fn a_message_with_neither_method_nor_reply_is_invalid_request() {
assert_eq!(
decode(br#"{"jsonrpc":"2.0","id":1}"#).unwrap_err(),
DecodeError::InvalidRequest("a message needs a method or a reply"),
);
}
#[test]
fn an_id_that_is_neither_number_nor_string_is_invalid_request() {
for input in [
r#"{"jsonrpc":"2.0","id":true,"method":"ping"}"#,
r#"{"jsonrpc":"2.0","id":null,"method":"ping"}"#,
r#"{"jsonrpc":"2.0","id":[1],"method":"ping"}"#,
r#"{"jsonrpc":"2.0","id":{"a":1},"method":"ping"}"#,
] {
assert_eq!(
decode(input.as_bytes()).unwrap_err(),
DecodeError::InvalidRequest("an id must be a number or a string"),
"input was {input}",
);
}
}
#[test]
fn a_fractional_id_is_invalid_request() {
assert_eq!(
decode(br#"{"jsonrpc":"2.0","id":1.5,"method":"ping"}"#).unwrap_err(),
DecodeError::InvalidRequest("a numeric id must not have a fractional part"),
);
}
#[test]
fn an_id_outside_i64_is_refused_rather_than_wrapped() {
let error =
decode(br#"{"jsonrpc":"2.0","id":9223372036854775808,"method":"ping"}"#).unwrap_err();
assert_matches!(error, DecodeError::InvalidRequest(_));
}
#[test]
fn a_wire_id_becomes_an_owned_request_id_without_loss() -> Result<(), TestError> {
for (input, expected) in [
(
r#"{"jsonrpc":"2.0","id":42,"method":"ping"}"#,
RequestId::number(42),
),
(
r#"{"jsonrpc":"2.0","id":"abc","method":"ping"}"#,
RequestId::text("abc"),
),
] {
let envelope = first(input.as_bytes())?;
let wire = envelope.id().ok_or("the envelope has an id")?;
let owned = match wire.as_number() {
Some(number) => RequestId::number(number),
None => RequestId::text(wire.as_text().ok_or("a text id has text")?),
};
assert_eq!(owned, expected, "input was {input}");
}
Ok(())
}
#[test]
fn an_id_writes_back_as_the_json_the_sender_sent() -> Result<(), TestError> {
let numeric = first(r#"{"jsonrpc":"2.0","id":42,"method":"ping"}"#.as_bytes())?;
assert_eq!(
numeric.id().ok_or("the envelope has an id")?.to_json(),
"42",
);
let text = first(r#"{"jsonrpc":"2.0","id":"abc","method":"ping"}"#.as_bytes())?;
assert_eq!(
text.id().ok_or("the envelope has an id")?.to_json(),
"\"abc\"",
"a string id must come back quoted, or a response would not be valid JSON"
);
Ok(())
}
#[test]
fn a_wire_id_rejects_what_the_spec_rejects() -> Result<(), TestError> {
let number = serde_json::value::to_raw_value(&1).map_err(TestError::from)?;
assert_matches!(WireId::from_raw(&number), Ok(WireId::Number(1)));
let text = serde_json::value::to_raw_value("x").map_err(TestError::from)?;
let id = WireId::from_raw(&text).map_err(TestError::from)?;
assert_eq!(id, WireId::text("x"), "an unescaped id borrows");
assert_matches!(id, WireId::Text(_));
let float = serde_json::value::to_raw_value(&1.5).map_err(TestError::from)?;
assert_eq!(WireId::from_raw(&float), Err(WireIdError::NotAnInteger));
let boolean = serde_json::value::to_raw_value(&true).map_err(TestError::from)?;
assert_eq!(WireId::from_raw(&boolean), Err(WireIdError::NotAnId));
Ok(())
}
#[test]
fn a_batch_is_recognised_and_ordered() -> Result<(), TestError> {
let input = br#"[
{"jsonrpc":"2.0","id":1,"method":"ping"},
{"jsonrpc":"2.0","method":"notifications/initialized"}
]"#;
let frame = decode(input)?;
assert!(frame.is_batch());
let envelopes = frame.envelopes();
assert_eq!(envelopes.len(), 2);
assert_eq!(
envelopes.first().ok_or("no first")?.id(),
Some(&WireId::Number(1))
);
assert_eq!(
envelopes.get(1).ok_or("no second")?.kind(),
MessageKind::Notification,
);
Ok(())
}
#[test]
fn an_empty_batch_is_invalid_request() {
assert_eq!(decode(b"[]").unwrap_err(), DecodeError::EmptyBatch);
assert_eq!(
DecodeError::EmptyBatch.code(),
IrErrorCode::InvalidRequest,
"an empty batch is a malformed frame, not a valid one with nothing in it"
);
}
#[test]
fn a_batch_is_rejected_whole_rather_than_elementwise() {
let input = br#"[{"jsonrpc":"2.0","id":1,"method":"ping"},{"jsonrpc":"1.0","id":2}]"#;
assert_eq!(
decode(input).unwrap_err(),
DecodeError::InvalidRequest("`jsonrpc` must be \"2.0\""),
);
}
#[test]
fn a_top_level_value_that_is_not_a_frame_is_invalid_request() {
for input in [b"42".as_slice(), b"\"hi\"".as_slice(), b"true".as_slice()] {
assert_matches!(
decode(input).unwrap_err(),
DecodeError::InvalidRequest(_),
"input was {:?}",
String::from_utf8_lossy(input),
);
}
}
#[test]
fn empty_input_is_a_parse_error() {
assert_eq!(decode(b"").unwrap_err(), DecodeError::Json);
assert_eq!(decode(b" \n ").unwrap_err(), DecodeError::Json);
}
#[test]
fn unknown_members_survive() -> Result<(), TestError> {
let input = br#"{"jsonrpc":"2.0","id":1,"method":"ping","$future":{"a":[1,2]}}"#;
let envelope = first(input.as_slice())?;
assert_eq!(envelope.kind(), MessageKind::Request);
Ok(())
}
#[test]
fn leading_whitespace_does_not_hide_the_frame_kind() -> Result<(), TestError> {
let batch = decode(b" \n\t [ {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"} ]")?;
assert!(batch.is_batch());
let single = first(b" {\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}".as_slice())?;
assert_eq!(single.kind(), MessageKind::Request);
Ok(())
}
#[test]
fn params_stay_undecoded_until_the_method_is_known() -> Result<(), TestError> {
let input = br#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"x","arguments":{}}}"#;
let envelope = first(input.as_slice())?;
let params = envelope.params().ok_or("the envelope has params")?;
assert_eq!(params.get(), r#"{"name":"x","arguments":{}}"#);
Ok(())
}
#[test]
fn a_message_with_null_params_and_no_params_are_different() -> Result<(), TestError> {
let omitted = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.as_bytes())?;
let null = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping","params":null}"#.as_bytes())?;
assert!(omitted.params().is_none(), "no params member at all");
assert!(
null.params().is_none(),
"an explicit null is the same absence, not a params value of null"
);
Ok(())
}
#[test]
fn a_routeable_message_reports_its_method_and_id() -> Result<(), TestError> {
let request = first(r#"{"jsonrpc":"2.0","id":1,"method":"ping"}"#.as_bytes())?;
assert_eq!(
request.routeable(),
Some((&Method::Borrowed("ping"), Some(&WireId::Number(1))))
);
let notification = first(r#"{"jsonrpc":"2.0","method":"ping"}"#.as_bytes())?;
assert_eq!(
notification.routeable(),
Some((&Method::Borrowed("ping"), None))
);
let reply = first(r#"{"jsonrpc":"2.0","id":1,"result":{}}"#.as_bytes())?;
assert_eq!(reply.routeable(), None, "a reply has no method to route");
Ok(())
}
#[test]
fn a_numeric_id_needs_no_allocation() -> Result<(), TestError> {
let envelope = first(br#"{"jsonrpc":"2.0","id":9007199254740993,"method":"ping"}"#)?;
let value = envelope
.id()
.and_then(WireId::as_number)
.ok_or("a numeric id must stay a number")?;
assert_eq!(
value, 9_007_199_254_740_993,
"and it must be exact past 2^53"
);
Ok(())
}
}