use serde::{Deserialize, Serialize};
pub const JSONRPC_VERSION: &str = "2.0";
pub mod error_codes {
pub const PARSE_ERROR: i64 = -32700;
pub const INVALID_REQUEST: i64 = -32600;
pub const METHOD_NOT_FOUND: i64 = -32601;
pub const INVALID_PARAMS: i64 = -32602;
pub const INTERNAL_ERROR: i64 = -32603;
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq, Hash)]
#[serde(untagged)]
pub enum JsonRpcId {
Number(u64),
Text(String),
}
impl JsonRpcId {
#[must_use]
pub const fn number(value: u64) -> Self {
Self::Number(value)
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct JsonRpcRequest {
pub jsonrpc: String,
pub id: JsonRpcId,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
impl JsonRpcRequest {
#[must_use]
pub fn new(
id: JsonRpcId,
method: impl Into<String>,
params: Option<serde_json::Value>,
) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_owned(),
id,
method: method.into(),
params,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct JsonRpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub params: Option<serde_json::Value>,
}
impl JsonRpcNotification {
#[must_use]
pub fn new(method: impl Into<String>, params: Option<serde_json::Value>) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_owned(),
method: method.into(),
params,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct JsonRpcError {
pub code: i64,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data: Option<serde_json::Value>,
}
impl JsonRpcError {
#[must_use]
pub fn new(code: i64, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
data: None,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]
pub struct JsonRpcResponse {
pub jsonrpc: String,
pub id: JsonRpcId,
#[serde(
default,
deserialize_with = "deserialize_present_value",
skip_serializing_if = "Option::is_none"
)]
pub result: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<JsonRpcError>,
}
fn deserialize_present_value<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
serde_json::Value::deserialize(deserializer).map(Some)
}
impl JsonRpcResponse {
#[must_use]
pub fn success(id: JsonRpcId, result: serde_json::Value) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_owned(),
id,
result: Some(result),
error: None,
}
}
#[must_use]
pub fn failure(id: JsonRpcId, error: JsonRpcError) -> Self {
Self {
jsonrpc: JSONRPC_VERSION.to_owned(),
id,
result: None,
error: Some(error),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum IncomingMessage {
Request(JsonRpcRequest),
Notification(JsonRpcNotification),
Response(JsonRpcResponse),
}
impl IncomingMessage {
pub fn from_value(value: serde_json::Value) -> Result<Self, serde_json::Error> {
let has_method = value.get("method").is_some();
let has_id = value.get("id").is_some_and(|id| !id.is_null());
if has_method {
if has_id {
serde_json::from_value(value).map(Self::Request)
} else {
serde_json::from_value(value).map(Self::Notification)
}
} else {
serde_json::from_value(value).map(Self::Response)
}
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::{
IncomingMessage, JsonRpcError, JsonRpcId, JsonRpcNotification, JsonRpcRequest,
JsonRpcResponse, error_codes,
};
#[test]
fn request_serialises_with_id_and_omits_absent_params() -> Result<(), serde_json::Error> {
let request = JsonRpcRequest::new(JsonRpcId::number(1), "run/execute", None);
let value = serde_json::to_value(&request)?;
assert_eq!(value["jsonrpc"], "2.0");
assert_eq!(value["id"], 1);
assert_eq!(value["method"], "run/execute");
assert!(value.get("params").is_none(), "absent params are omitted");
Ok(())
}
#[test]
fn notification_never_carries_an_id() -> Result<(), serde_json::Error> {
let notification = JsonRpcNotification::new("event/message", Some(json!({ "text": "hi" })));
let value = serde_json::to_value(¬ification)?;
assert!(value.get("id").is_none(), "notifications carry no id");
assert_eq!(value["params"]["text"], "hi");
Ok(())
}
#[test]
fn classifies_request_notification_and_response() -> Result<(), serde_json::Error> {
let request = IncomingMessage::from_value(json!({
"jsonrpc": "2.0", "id": 7, "method": "intervene/inject", "params": {}
}))?;
assert!(matches!(request, IncomingMessage::Request(_)));
let notification = IncomingMessage::from_value(json!({
"jsonrpc": "2.0", "method": "event/stop", "params": {}
}))?;
assert!(matches!(notification, IncomingMessage::Notification(_)));
let response = IncomingMessage::from_value(json!({
"jsonrpc": "2.0", "id": 7, "result": { "ok": true }
}))?;
assert!(matches!(response, IncomingMessage::Response(_)));
Ok(())
}
#[test]
fn a_null_id_is_treated_as_a_notification_not_a_request() -> Result<(), serde_json::Error> {
let message = IncomingMessage::from_value(json!({
"jsonrpc": "2.0", "id": null, "method": "event/raw", "params": {}
}))?;
assert!(
matches!(message, IncomingMessage::Notification(_)),
"a null id must not make a method-bearing frame a correlated request"
);
Ok(())
}
#[test]
fn error_response_carries_result_none() {
let response = JsonRpcResponse::failure(
JsonRpcId::number(3),
JsonRpcError::new(error_codes::METHOD_NOT_FOUND, "method not found"),
);
assert!(response.result.is_none());
assert!(
matches!(response.error, Some(error) if error.code == error_codes::METHOD_NOT_FOUND),
"an error response carries the code"
);
}
#[test]
fn a_present_null_result_decodes_as_some_null() -> Result<(), serde_json::Error> {
let response: JsonRpcResponse = serde_json::from_value(json!({
"jsonrpc": "2.0", "id": 1, "result": null
}))?;
assert_eq!(response.result, Some(serde_json::Value::Null));
let wire = serde_json::to_value(&response)?;
assert!(
wire.as_object()
.is_some_and(|frame| frame.contains_key("result")),
"a present null result round-trips with the key on the wire"
);
Ok(())
}
#[test]
fn an_absent_result_decodes_as_none() -> Result<(), serde_json::Error> {
let response: JsonRpcResponse = serde_json::from_value(json!({
"jsonrpc": "2.0", "id": 1
}))?;
assert!(response.result.is_none());
assert!(response.error.is_none());
let wire = serde_json::to_value(&response)?;
assert!(
wire.as_object()
.is_some_and(|frame| !frame.contains_key("result")),
"an absent result stays absent on the wire"
);
Ok(())
}
#[test]
fn string_ids_round_trip() -> Result<(), serde_json::Error> {
let id = JsonRpcId::Text("abc".to_owned());
let value = serde_json::to_value(&id)?;
let decoded: JsonRpcId = serde_json::from_value(value)?;
assert_eq!(decoded, id);
Ok(())
}
}