use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RpcId {
Number(u64),
String(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcRequest {
pub jsonrpc: String,
pub id: RpcId,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcResponse {
pub jsonrpc: String,
pub id: RpcId,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<RpcError>,
}
#[derive(Debug, Clone, Serialize, Deserialize, thiserror::Error)]
#[error("JSON-RPC error {code}: {message}")]
pub struct RpcError {
pub code: i32,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcNotification {
pub jsonrpc: String,
pub method: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
impl RpcError {
pub const PARSE_ERROR: i32 = -32700;
pub const INVALID_REQUEST: i32 = -32600;
pub const METHOD_NOT_FOUND: i32 = -32601;
pub const INVALID_PARAMS: i32 = -32602;
pub const INTERNAL_ERROR: i32 = -32603;
pub const NOT_FOUND: i32 = -32001;
pub const PERMISSION_DENIED: i32 = -32002;
pub const INVALID_STATE: i32 = -32003;
pub const UNSUPPORTED: i32 = -32004;
pub fn method_not_found(method: &str) -> Self {
Self {
code: Self::METHOD_NOT_FOUND,
message: format!("Method not found: {}", method),
data: None,
}
}
pub fn internal(msg: impl Into<String>) -> Self {
Self {
code: Self::INTERNAL_ERROR,
message: msg.into(),
data: None,
}
}
}
impl RpcResponse {
pub fn success(id: RpcId, result: Value) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: Some(result),
error: None,
}
}
pub fn error_response(id: RpcId, err: RpcError) -> Self {
Self {
jsonrpc: "2.0".into(),
id,
result: None,
error: Some(err),
}
}
}
impl RpcRequest {
pub fn new(id: impl Into<RpcId>, method: impl Into<String>, params: Option<Value>) -> Self {
Self {
jsonrpc: "2.0".into(),
id: id.into(),
method: method.into(),
params,
}
}
}
impl From<u64> for RpcId {
fn from(n: u64) -> Self {
RpcId::Number(n)
}
}
impl From<String> for RpcId {
fn from(s: String) -> Self {
RpcId::String(s)
}
}
impl From<&str> for RpcId {
fn from(s: &str) -> Self {
RpcId::String(s.to_owned())
}
}
#[derive(Debug)]
pub enum IncomingMessage {
Request { id: RpcId, method: String, params: Option<Value> },
Notification { method: String, params: Option<Value> },
Response { id: RpcId, result: Option<Value>, error: Option<RpcError> },
Legacy(String),
}
pub fn classify_line(line: &str) -> IncomingMessage {
let trimmed = line.trim();
if trimmed.is_empty() {
return IncomingMessage::Legacy(line.to_owned());
}
let val: Value = match serde_json::from_str(trimmed) {
Ok(v) => v,
Err(_) => return IncomingMessage::Legacy(line.to_owned()),
};
if val.get("jsonrpc").and_then(|v| v.as_str()) != Some("2.0") {
return IncomingMessage::Legacy(line.to_owned());
}
let id = val.get("id").and_then(parse_rpc_id);
let method = val
.get("method")
.and_then(|v| v.as_str())
.map(|s| s.to_owned());
let params = val.get("params").cloned();
match (id, method) {
(Some(id), Some(method)) => IncomingMessage::Request { id, method, params },
(None, Some(method)) => IncomingMessage::Notification { method, params },
(Some(id), None) => {
let result = val.get("result").cloned();
let error = val
.get("error")
.and_then(|e| serde_json::from_value(e.clone()).ok());
IncomingMessage::Response { id, result, error }
}
(None, None) => IncomingMessage::Legacy(line.to_owned()),
}
}
fn parse_rpc_id(val: &Value) -> Option<RpcId> {
match val {
Value::Number(n) => n.as_u64().map(RpcId::Number),
Value::String(s) => Some(RpcId::String(s.clone())),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn classify_request() {
let line = r#"{"jsonrpc":"2.0","id":1,"method":"fs/read_text_file","params":{"path":"/tmp/x"}}"#;
match classify_line(line) {
IncomingMessage::Request { id, method, params } => {
assert_eq!(id, RpcId::Number(1));
assert_eq!(method, "fs/read_text_file");
assert!(params.is_some());
}
other => panic!("expected Request, got {:?}", other),
}
}
#[test]
fn classify_request_string_id() {
let line = r#"{"jsonrpc":"2.0","id":"abc","method":"ping","params":null}"#;
match classify_line(line) {
IncomingMessage::Request { id, .. } => {
assert_eq!(id, RpcId::String("abc".into()));
}
other => panic!("expected Request, got {:?}", other),
}
}
#[test]
fn classify_notification() {
let line = r#"{"jsonrpc":"2.0","method":"session/update","params":{"type":"text"}}"#;
match classify_line(line) {
IncomingMessage::Notification { method, params } => {
assert_eq!(method, "session/update");
assert!(params.is_some());
}
other => panic!("expected Notification, got {:?}", other),
}
}
#[test]
fn classify_response_success() {
let line = r#"{"jsonrpc":"2.0","id":42,"result":{"content":"hello"}}"#;
match classify_line(line) {
IncomingMessage::Response { id, result, error } => {
assert_eq!(id, RpcId::Number(42));
assert!(result.is_some());
assert!(error.is_none());
}
other => panic!("expected Response, got {:?}", other),
}
}
#[test]
fn classify_response_error() {
let line = r#"{"jsonrpc":"2.0","id":7,"error":{"code":-32601,"message":"Method not found"}}"#;
match classify_line(line) {
IncomingMessage::Response { id, result, error } => {
assert_eq!(id, RpcId::Number(7));
assert!(result.is_none());
let err = error.unwrap();
assert_eq!(err.code, -32601);
}
other => panic!("expected Response, got {:?}", other),
}
}
#[test]
fn classify_legacy_non_json() {
let line = "this is plain text";
assert!(matches!(classify_line(line), IncomingMessage::Legacy(_)));
}
#[test]
fn classify_legacy_json_no_jsonrpc_field() {
let line = r#"{"type":"text","content":"hello"}"#;
assert!(matches!(classify_line(line), IncomingMessage::Legacy(_)));
}
#[test]
fn classify_legacy_jsonrpc_v1() {
let line = r#"{"jsonrpc":"1.0","id":1,"method":"test"}"#;
assert!(matches!(classify_line(line), IncomingMessage::Legacy(_)));
}
#[test]
fn classify_empty_line() {
assert!(matches!(classify_line(""), IncomingMessage::Legacy(_)));
assert!(matches!(classify_line(" "), IncomingMessage::Legacy(_)));
}
#[test]
fn rpc_response_no_null_fields_serialized() {
let resp = RpcResponse::success(RpcId::Number(1), json!({"ok": true}));
let s = serde_json::to_string(&resp).unwrap();
assert!(!s.contains("\"error\""), "error field should be absent");
assert!(s.contains("\"result\""));
}
#[test]
fn rpc_error_response_no_null_result() {
let resp = RpcResponse::error_response(RpcId::Number(1), RpcError::method_not_found("foo"));
let s = serde_json::to_string(&resp).unwrap();
assert!(!s.contains("\"result\""), "result field should be absent");
assert!(s.contains("\"error\""));
}
}