use serde_json::{Map, Value, json};
use super::{
ExtensionValue, Extensions, IrError, IrErrorCode, IrMessage, IrNotification, IrParams,
IrRequest, IrResult, JsonObject, MethodName,
};
use crate::domain::RequestId;
const KNOWN: [&str; 6] = ["jsonrpc", "id", "method", "params", "result", "error"];
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum EnvelopeError {
#[error("the object is not a JSON-RPC request, notification, result or error")]
NotAMessage,
#[error("the method member is {found}, not a string")]
MethodNotAString {
found: &'static str,
},
#[error("the id member is {found}, which JSON-RPC does not allow")]
IdNotAllowed {
found: &'static str,
},
#[error("the error code is {found}, not an integer")]
CodeNotAnInteger {
found: &'static str,
},
}
pub fn lift(object: &Map<String, Value>) -> Result<IrMessage, EnvelopeError> {
let mut extensions = Extensions::empty();
for (key, value) in object {
if !KNOWN.contains(&key.as_str()) {
extensions.insert(key.clone(), ExtensionValue::from(value.clone()));
}
}
let id = match object.get("id") {
None | Some(Value::Null) => None,
Some(Value::String(text)) => Some(RequestId::text(text)),
Some(Value::Number(number)) => Some(
number
.as_i64()
.map_or_else(|| RequestId::text(number.to_string()), RequestId::number),
),
Some(other) => {
return Err(EnvelopeError::IdNotAllowed {
found: type_name(other),
});
}
};
if let Some(method) = object.get("method") {
let name = match method {
Value::String(text) => text.clone(),
other => {
return Err(EnvelopeError::MethodNotAString {
found: type_name(other),
});
}
};
let method = MethodName::new(&name).map_err(|_| EnvelopeError::NotAMessage)?;
let params = lift_params(object.get("params"));
return Ok(match id {
Some(id) => IrMessage::Request(IrRequest {
method,
id: Some(id),
params,
extensions,
}),
None => IrMessage::Notification(IrNotification {
method,
params,
extensions,
}),
});
}
if let Some(result) = object.get("result") {
return Ok(IrMessage::Result(IrResult {
id: id.ok_or(EnvelopeError::NotAMessage)?,
params: lift_params(Some(result)),
extensions,
}));
}
if let Some(error) = object.get("error") {
return Ok(IrMessage::Error(lift_error(error, id, extensions)?));
}
Err(EnvelopeError::NotAMessage)
}
fn lift_params(value: Option<&Value>) -> IrParams {
match value {
Some(Value::Object(object)) => {
let mut extensions = Extensions::empty();
extensions.absorb_json(object);
IrParams::from_extensions(extensions)
}
_ => IrParams::empty(),
}
}
fn lift_error(
value: &Value,
id: Option<RequestId>,
extensions: Extensions,
) -> Result<IrError, EnvelopeError> {
let Value::Object(object) = value else {
return Err(EnvelopeError::NotAMessage);
};
let mut extensions = extensions;
for (key, member) in object {
if !matches!(key.as_str(), "code" | "message" | "data") {
extensions.insert(key.clone(), ExtensionValue::from(member.clone()));
}
}
let code = match object.get("code") {
Some(Value::Number(number)) => IrErrorCode::from_code(
number
.as_i64()
.ok_or(EnvelopeError::CodeNotAnInteger { found: "a float" })?,
),
Some(other) => {
return Err(EnvelopeError::CodeNotAnInteger {
found: type_name(other),
});
}
None => IrErrorCode::InternalError,
};
let message = object
.get("message")
.and_then(Value::as_str)
.unwrap_or_default()
.to_owned();
let data = object
.get("data")
.map(|value| JsonObject::new(value.to_string().as_str()))
.transpose()
.map_err(|_| EnvelopeError::NotAMessage)?;
Ok(IrError {
id,
code,
message,
data,
extensions,
})
}
#[must_use]
pub fn lower(message: &IrMessage) -> Map<String, Value> {
let mut object = Map::new();
object.insert("jsonrpc".to_owned(), json!("2.0"));
match message {
IrMessage::Request(request) => {
object.insert("method".to_owned(), json!(request.method.as_str()));
set_id(&mut object, request.id.as_ref());
insert_params(&mut object, &request.params);
absorb(&mut object, &request.extensions);
}
IrMessage::Notification(notification) => {
object.insert("method".to_owned(), json!(notification.method.as_str()));
insert_params(&mut object, ¬ification.params);
absorb(&mut object, ¬ification.extensions);
}
IrMessage::Result(result) => {
set_id(&mut object, Some(&result.id));
object.insert("result".to_owned(), params_to_value(&result.params));
absorb(&mut object, &result.extensions);
}
IrMessage::Error(error) => {
if let Some(id) = &error.id {
set_id(&mut object, Some(id));
} else {
object.insert("id".to_owned(), Value::Null);
}
let mut inner = Map::new();
inner.insert("code".to_owned(), json!(error.code.code()));
inner.insert("message".to_owned(), json!(error.message));
if let Some(data) = &error.data {
inner.insert(
"data".to_owned(),
serde_json::from_str(data.as_str()).unwrap_or(Value::Null),
);
}
absorb(&mut inner, &error.extensions);
object.insert("error".to_owned(), Value::Object(inner));
}
}
object
}
fn set_id(object: &mut Map<String, Value>, id: Option<&RequestId>) {
if let Some(id) = id {
object.insert(
"id".to_owned(),
serde_json::from_str(&id.to_json()).unwrap_or(Value::Null),
);
}
}
fn insert_params(object: &mut Map<String, Value>, params: &IrParams) {
if !params.as_extensions().is_empty() {
object.insert("params".to_owned(), params_to_value(params));
}
}
fn params_to_value(params: &IrParams) -> Value {
Value::Object(params.as_extensions().to_json_object())
}
fn absorb(object: &mut Map<String, Value>, extensions: &Extensions) {
for (key, value) in extensions.to_json_object() {
object.entry(key).or_insert(value);
}
}
const fn type_name(value: &Value) -> &'static str {
match value {
Value::Null => "null",
Value::Bool(_) => "a boolean",
Value::Number(_) => "a number",
Value::String(_) => "a string",
Value::Array(_) => "an array",
Value::Object(_) => "an object",
}
}
#[cfg(test)]
mod tests {
use serde_json::{Value, json};
use super::{EnvelopeError, lift, lower};
type TestError = Box<dyn std::error::Error>;
fn round_trip(value: Value) -> Result<Value, TestError> {
let Value::Object(object) = value else {
return Err("a fixture that is not an object".into());
};
let message = lift(&object)?;
Ok(Value::Object(lower(&message)))
}
#[test]
fn an_unmodelled_directive_survives_the_round_trip() -> Result<(), TestError> {
let sent = json!({
"jsonrpc": "2.0",
"id": 7,
"method": "tools/call",
"params": {
"name": "echo",
"arguments": {"text": "hi"},
"_meta": {"com.example/trace": {"span": 12}}
},
"x-vendor-directive": {"anything": [1, 2, 3]},
"future-top-level": "surprise"
});
let back = round_trip(sent.clone())?;
assert_eq!(back, sent, "the bridge altered the wire form");
Ok(())
}
#[test]
fn an_unmodelled_result_member_survives_the_round_trip() -> Result<(), TestError> {
let sent = json!({
"jsonrpc": "2.0",
"id": "abc",
"result": {"content": [], "vendorExtra": {"a": 1}},
"x-trace": "t-1"
});
assert_eq!(round_trip(sent.clone())?, sent);
Ok(())
}
#[test]
fn lowering_keeps_every_member() -> Result<(), TestError> {
let sent = json!({
"jsonrpc": "2.0",
"id": 1,
"method": "ping",
"zeta": 1,
"alpha": 2,
"nested": {"b": [1, 2], "a": null}
});
let back = round_trip(sent.clone())?;
let (Value::Object(forwarded), Value::Object(original)) = (&back, &sent) else {
return Err("a fixture that is not an object".into());
};
assert_eq!(
forwarded.len(),
original.len(),
"lowering dropped or invented a member: {forwarded:?}"
);
for (key, value) in original {
assert_eq!(
forwarded.get(key),
Some(value),
"member {key:?} did not survive"
);
}
Ok(())
}
#[test]
fn a_large_integer_id_survives_exactly() -> Result<(), TestError> {
let beyond_float_precision = 9_007_199_254_740_993_i64;
let sent = json!({
"jsonrpc": "2.0",
"id": beyond_float_precision,
"method": "ping"
});
assert_eq!(round_trip(sent.clone())?, sent);
Ok(())
}
#[test]
fn a_notification_stays_a_notification() -> Result<(), TestError> {
let sent = json!({"jsonrpc": "2.0", "method": "notifications/initialized", "note": 1});
let back = round_trip(sent.clone())?;
assert_eq!(back, sent);
assert!(
back.get("id").is_none(),
"a notification gained an id it never had"
);
Ok(())
}
#[test]
fn a_null_id_is_kept() -> Result<(), TestError> {
let sent =
json!({"jsonrpc": "2.0", "id": null, "error": {"code": -32700, "message": "parse"}});
assert_eq!(round_trip(sent.clone())?, sent);
Ok(())
}
#[test]
fn error_data_survives() -> Result<(), TestError> {
let sent = json!({
"jsonrpc": "2.0",
"id": 3,
"error": {"code": -32602, "message": "bad", "data": {"detail": [1, {"x": null}]}}
});
assert_eq!(round_trip(sent.clone())?, sent);
Ok(())
}
#[test]
fn malformed_envelopes_are_refused_by_kind() -> Result<(), TestError> {
let cases: [(&str, Value); 3] = [
("nothing", json!({"jsonrpc": "2.0"})),
(
"method type",
json!({"jsonrpc": "2.0", "id": 1, "method": 5}),
),
(
"id type",
json!({"jsonrpc": "2.0", "id": [], "method": "ping"}),
),
];
for (label, value) in cases {
let Value::Object(object) = &value else {
return Err("a fixture that is not an object".into());
};
let Err(refusal) = lift(object) else {
return Err(format!("{label} should have been refused").into());
};
match (label, refusal) {
("nothing", EnvelopeError::NotAMessage) => {}
("method type", EnvelopeError::MethodNotAString { found }) => {
assert_eq!(found, "a number");
}
("id type", EnvelopeError::IdNotAllowed { found }) => {
assert_eq!(found, "an array");
}
_ => return Err(format!("{label} reported the wrong refusal").into()),
}
}
Ok(())
}
}