use serde_json::{Map, Value};
use crate::domain::ir::{IrErrorCode, JsonObject};
use crate::domain::jsonrpc::WireId;
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
pub enum ReplyError {
#[error("the result could not be read back as JSON")]
UnreadableResult,
}
fn base_members() -> Map<String, Value> {
let mut members = Map::new();
members.insert("jsonrpc".to_owned(), Value::from("2.0"));
members
}
fn reply_members(id: &WireId<'_>) -> Map<String, Value> {
let mut members = base_members();
members.insert("id".to_owned(), id_value(id));
members
}
fn id_value(id: &WireId<'_>) -> Value {
match id {
WireId::Number(value) => Value::from(*value),
WireId::Text(text) => Value::from(text.as_ref()),
}
}
fn error_member(code: IrErrorCode, message: &str) -> Value {
error_member_with_data(code, message, None)
}
fn error_member_with_data(code: IrErrorCode, message: &str, data: Option<&Value>) -> Value {
let mut error = Map::new();
error.insert("code".to_owned(), Value::from(code.code()));
error.insert("message".to_owned(), Value::from(message));
if let Some(data) = data {
error.insert("data".to_owned(), data.clone());
}
Value::Object(error)
}
fn read_back(result: &JsonObject) -> Result<Value, ReplyError> {
serde_json::from_str::<Value>(result.as_str()).map_err(|_| ReplyError::UnreadableResult)
}
pub fn result_reply(id: &WireId<'_>, result: &JsonObject) -> Result<Value, ReplyError> {
let mut members = reply_members(id);
members.insert("result".to_owned(), read_back(result)?);
Ok(Value::Object(members))
}
#[must_use]
pub fn error_reply(id: &WireId<'_>, code: IrErrorCode, message: &str) -> Value {
let mut members = reply_members(id);
members.insert("error".to_owned(), error_member(code, message));
Value::Object(members)
}
#[must_use]
pub fn error_reply_with_data(
id: &WireId<'_>,
code: IrErrorCode,
message: &str,
data: &JsonObject,
) -> Value {
let Ok(data) = read_back(data) else {
return error_reply(id, code, message);
};
let mut members = reply_members(id);
members.insert(
"error".to_owned(),
error_member_with_data(code, message, Some(&data)),
);
Value::Object(members)
}
#[must_use]
pub fn unidentified_error_reply(code: IrErrorCode, message: &str) -> Value {
let mut members = base_members();
members.insert("id".to_owned(), Value::Null);
members.insert("error".to_owned(), error_member(code, message));
Value::Object(members)
}
#[cfg(test)]
mod tests {
use super::{ReplyError, base_members, error_reply, result_reply, unidentified_error_reply};
use crate::domain::ir::{IrErrorCode, JsonObject};
use crate::domain::jsonrpc::WireId;
#[derive(Debug)]
enum Failure {
Object(crate::domain::ir::JsonObjectError),
Reply(ReplyError),
Json(serde_json::Error),
}
impl std::fmt::Display for Failure {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Object(error) => write!(f, "bad fixture: {error}"),
Self::Reply(error) => write!(f, "reply refused: {error}"),
Self::Json(error) => write!(f, "a reply did not parse: {error}"),
}
}
}
impl std::error::Error for Failure {}
impl From<crate::domain::ir::JsonObjectError> for Failure {
fn from(error: crate::domain::ir::JsonObjectError) -> Self {
Self::Object(error)
}
}
impl From<ReplyError> for Failure {
fn from(error: ReplyError) -> Self {
Self::Reply(error)
}
}
impl From<serde_json::Error> for Failure {
fn from(error: serde_json::Error) -> Self {
Self::Json(error)
}
}
#[test]
fn a_result_reply_carries_all_three() -> Result<(), Failure> {
let id = WireId::text("r1");
let result = JsonObject::new(r#"{"content":[]}"#)?;
let document = result_reply(&id, &result)?.to_string();
assert_eq!(
document,
r#"{"id":"r1","jsonrpc":"2.0","result":{"content":[]}}"#,
);
Ok(())
}
#[test]
fn the_id_keeps_its_type() -> Result<(), Failure> {
let numeric = result_reply(&WireId::Number(7), &JsonObject::default())?.to_string();
let textual = result_reply(&WireId::text("7"), &JsonObject::default())?.to_string();
assert!(numeric.contains(r#""id":7"#), "{numeric}");
assert!(textual.contains(r#""id":"7""#), "{textual}");
Ok(())
}
#[test]
fn an_escaped_id_is_escaped_as_json() -> Result<(), Failure> {
let id = WireId::text("a\u{7f}b\"c");
let document = result_reply(&id, &JsonObject::default())?.to_string();
assert!(!document.contains("u{7f}"), "{document}");
let read_back = serde_json::from_str::<serde_json::Value>(&document)?;
assert_eq!(read_back.get("id"), Some(&serde_json::json!("a\u{7f}b\"c")));
Ok(())
}
#[test]
fn a_pretty_printed_result_stays_on_one_line() -> Result<(), Failure> {
let result = JsonObject::new("{\n \"a\": 1\n}")?;
let document = result_reply(&WireId::Number(1), &result)?.to_string();
assert!(!document.contains('\n'), "{document}");
Ok(())
}
#[test]
fn an_error_reply_carries_the_numeric_code() {
let document =
error_reply(&WireId::Number(1), IrErrorCode::InvalidParams, "nope").to_string();
assert_eq!(
document,
r#"{"error":{"code":-32602,"message":"nope"},"id":1,"jsonrpc":"2.0"}"#,
);
}
#[test]
fn an_error_message_is_escaped() {
let document = error_reply(
&WireId::Number(1),
IrErrorCode::InternalError,
"line one\nline two",
)
.to_string();
assert!(!document.contains('\n'), "{document}");
assert!(document.contains(r"\n"), "{document}");
}
#[test]
fn an_unidentified_error_carries_a_null_id() {
let document =
unidentified_error_reply(IrErrorCode::ParseError, "the frame is not valid JSON");
assert_eq!(document.get("id"), Some(&serde_json::Value::Null));
}
#[test]
fn both_builders_start_from_the_same_members() {
let base = base_members();
let identified = error_reply(&WireId::Number(1), IrErrorCode::InternalError, "x");
let unidentified = unidentified_error_reply(IrErrorCode::InternalError, "x");
for document in [&identified, &unidentified] {
assert_eq!(document.get("jsonrpc"), base.get("jsonrpc"));
}
}
}