use serde_json::{Map, Value};
use crate::domain::RequestId;
use crate::domain::ir::{IrErrorCode, JsonObject, MethodName};
const NO_PEER_MESSAGE: &str = "the peer reported an error with no message";
pub fn request_document(
id: &RequestId,
method: &MethodName,
params: &JsonObject,
) -> Result<Value, RequestError> {
let mut members = base_members();
members.insert("id".to_owned(), id_value(id));
members.insert("method".to_owned(), Value::from(method.as_str()));
members.insert("params".to_owned(), read_back(params)?);
Ok(Value::Object(members))
}
pub fn notification_document(
method: &MethodName,
params: &JsonObject,
) -> Result<Value, RequestError> {
let mut members = base_members();
members.insert("method".to_owned(), Value::from(method.as_str()));
members.insert("params".to_owned(), read_back(params)?);
Ok(Value::Object(members))
}
pub fn reply_result(frame: &str, expected: &RequestId) -> Result<JsonObject, RequestError> {
let document: Value = serde_json::from_str(frame).map_err(|_| RequestError::UnreadableFrame)?;
let Value::Object(members) = &document else {
return Err(RequestError::UnreadableFrame);
};
match members.get("id") {
None => return Err(RequestError::MissingId),
Some(found) if found != &id_value(expected) => {
return Err(RequestError::IdMismatch {
expected: expected.redacted(),
found: redact_member(found),
});
}
Some(_) => {}
}
if let Some(error) = members.get("error") {
return Err(peer_error(error));
}
let Some(result) = members.get("result") else {
return Err(RequestError::MissingResult);
};
JsonObject::from_value(result.clone()).map_err(|_| RequestError::ResultNotAnObject)
}
fn base_members() -> Map<String, Value> {
let mut members = Map::new();
members.insert("jsonrpc".to_owned(), Value::from("2.0"));
members
}
fn id_value(id: &RequestId) -> Value {
match id {
RequestId::Number(value) => Value::from(*value),
RequestId::Text(text) => Value::from(text.as_ref()),
}
}
fn read_back(params: &JsonObject) -> Result<Value, RequestError> {
serde_json::from_str::<Value>(params.as_str()).map_err(|_| RequestError::UnreadableParams)
}
fn peer_error(error: &Value) -> RequestError {
let Value::Object(error) = error else {
return RequestError::UnreadableFrame;
};
let Some(code) = error.get("code").and_then(Value::as_i64) else {
return RequestError::UnreadableFrame;
};
let message = error
.get("message")
.and_then(Value::as_str)
.unwrap_or(NO_PEER_MESSAGE);
RequestError::PeerError {
code: IrErrorCode::from_code(code),
message: message.to_owned(),
data: error.get("data").map(Value::to_string),
}
}
fn redact_member(found: &Value) -> String {
match found {
Value::String(text) => RequestId::text(text).redacted(),
other => other.to_string(),
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum RequestError {
#[error("the params could not be read back as JSON")]
UnreadableParams,
#[error("the frame is not a JSON object")]
UnreadableFrame,
#[error("the frame carries no id")]
MissingId,
#[error("the frame answers id {found}, not {expected}")]
IdMismatch {
expected: String,
found: String,
},
#[error("the frame carries neither a result nor an error")]
MissingResult,
#[error("the peer's result is not a JSON object")]
ResultNotAnObject,
#[error("the peer reported {code:?}: {message}")]
PeerError {
code: IrErrorCode,
message: String,
data: Option<String>,
},
}
#[cfg(test)]
mod tests {
use serde_json::Value;
use assert_matches::assert_matches;
use super::{RequestError, notification_document, reply_result, request_document};
use crate::domain::RequestId;
use crate::domain::ir::{
IrErrorCode, JsonObject, JsonObjectError, MethodName, MethodNameError,
};
#[derive(Debug)]
#[expect(dead_code, reason = "the payloads are read by the derived Debug")]
enum Failure {
Frame(RequestError),
Method(MethodNameError),
Params(JsonObjectError),
}
impl From<RequestError> for Failure {
fn from(error: RequestError) -> Self {
Self::Frame(error)
}
}
impl From<MethodNameError> for Failure {
fn from(error: MethodNameError) -> Self {
Self::Method(error)
}
}
impl From<JsonObjectError> for Failure {
fn from(error: JsonObjectError) -> Self {
Self::Params(error)
}
}
#[test]
fn a_request_carries_the_jsonrpc_member_id_method_and_params() -> Result<(), Failure> {
let method = MethodName::new("tools/list")?;
let params = JsonObject::new(r#"{"cursor":"c"}"#)?;
let document = request_document(&RequestId::number(7), &method, ¶ms)?.to_string();
assert_eq!(
document,
r#"{"id":7,"jsonrpc":"2.0","method":"tools/list","params":{"cursor":"c"}}"#
);
Ok(())
}
#[test]
fn a_notification_has_no_id() -> Result<(), Failure> {
let method = MethodName::new("notifications/initialized")?;
let document = notification_document(&method, &JsonObject::default())?.to_string();
assert!(!document.contains("\"id\""), "{document}");
Ok(())
}
#[test]
fn a_string_id_is_escaped_as_json() -> Result<(), Failure> {
let method = MethodName::new("tools/list")?;
let id = RequestId::text("a\u{7f}b\"c");
let document = request_document(&id, &method, &JsonObject::default())?.to_string();
assert!(!document.contains("u{7f}"), "{document}");
let read_back: Value =
serde_json::from_str(&document).map_err(|_| RequestError::UnreadableFrame)?;
assert_eq!(read_back.get("id"), Some(&Value::from("a\u{7f}b\"c")));
Ok(())
}
#[test]
fn a_result_is_read_back_as_an_object() -> Result<(), Failure> {
let frame = r#"{"id":1,"jsonrpc":"2.0","result":{"tools":[]}}"#;
let result = reply_result(frame, &RequestId::number(1))?;
assert_eq!(result.as_str(), r#"{"tools":[]}"#);
Ok(())
}
#[test]
fn a_reply_for_another_request_is_refused() {
let frame = r#"{"id":2,"jsonrpc":"2.0","result":{}}"#;
assert_matches!(
reply_result(frame, &RequestId::number(1)),
Err(RequestError::IdMismatch { .. })
);
}
#[test]
fn a_text_id_is_not_its_numeric_spelling() {
let frame = r#"{"id":7,"jsonrpc":"2.0","result":{}}"#;
assert_matches!(
reply_result(frame, &RequestId::text("7")),
Err(RequestError::IdMismatch { .. })
);
}
#[test]
fn an_error_reply_carries_the_code_the_peer_sent() {
let frame =
r#"{"error":{"code":-32601,"message":"no such method"},"id":1,"jsonrpc":"2.0"}"#;
assert_matches!(
reply_result(frame, &RequestId::number(1)),
Err(RequestError::PeerError {
code: IrErrorCode::MethodNotFound,
..
})
);
}
#[test]
fn a_frame_that_is_not_an_object_is_refused() {
assert_matches!(
reply_result("not json at all", &RequestId::number(1)),
Err(RequestError::UnreadableFrame)
);
assert_matches!(
reply_result("[1,2,3]", &RequestId::number(1)),
Err(RequestError::UnreadableFrame)
);
}
#[test]
fn a_frame_with_neither_result_nor_error_is_refused() {
let frame = r#"{"id":1,"jsonrpc":"2.0"}"#;
assert_matches!(
reply_result(frame, &RequestId::number(1)),
Err(RequestError::MissingResult)
);
}
#[test]
fn a_result_that_is_not_an_object_is_refused() {
let frame = r#"{"id":1,"jsonrpc":"2.0","result":[1,2,3]}"#;
assert_matches!(
reply_result(frame, &RequestId::number(1)),
Err(RequestError::ResultNotAnObject)
);
}
}