use std::io::{BufRead, Write};
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Framing {
Headers,
Lines,
}
#[derive(Debug, Clone, Deserialize)]
pub struct Incoming {
#[serde(default)]
pub id: Option<Value>,
pub method: String,
#[serde(default)]
pub params: Value,
}
impl Incoming {
#[must_use]
pub const fn expects_reply(&self) -> bool {
self.id.is_some()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Outgoing {
pub jsonrpc: &'static str,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub result: Option<Value>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<ErrorObject>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub params: Option<Value>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ErrorObject {
pub code: i32,
pub message: String,
}
pub mod codes {
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;
}
impl Outgoing {
#[must_use]
pub fn result(id: Option<Value>, result: Value) -> Self {
Self {
jsonrpc: "2.0",
id,
result: Some(result),
error: None,
method: None,
params: None,
}
}
#[must_use]
pub fn error(id: Option<Value>, code: i32, message: impl Into<String>) -> Self {
Self {
jsonrpc: "2.0",
id,
result: None,
error: Some(ErrorObject {
code,
message: message.into(),
}),
method: None,
params: None,
}
}
#[must_use]
pub fn notification(method: impl Into<String>, params: Value) -> Self {
Self {
jsonrpc: "2.0",
id: None,
result: None,
error: None,
method: Some(method.into()),
params: Some(params),
}
}
}
pub fn read(input: &mut impl BufRead, framing: Framing) -> std::io::Result<Option<String>> {
match framing {
Framing::Lines => {
let mut line = String::new();
if input.read_line(&mut line)? == 0 {
return Ok(None);
}
let line = line.trim().to_owned();
if line.is_empty() {
return read(input, framing);
}
Ok(Some(line))
}
Framing::Headers => {
let mut length: Option<usize> = None;
loop {
let mut line = String::new();
if input.read_line(&mut line)? == 0 {
return Ok(None);
}
let line = line.trim_end_matches(['\r', '\n']);
if line.is_empty() {
break;
}
if let Some((name, value)) = line.split_once(':')
&& name.trim().eq_ignore_ascii_case("content-length")
{
length = value.trim().parse().ok();
}
}
let Some(length) = length else {
return Ok(None);
};
let mut body = vec![0_u8; length];
std::io::Read::read_exact(input, &mut body)?;
Ok(Some(String::from_utf8_lossy(&body).into_owned()))
}
}
}
pub fn write(output: &mut impl Write, framing: Framing, message: &Outgoing) -> std::io::Result<()> {
let body = serde_json::to_string(message).unwrap_or_else(|_| {
String::from(r#"{"jsonrpc":"2.0","error":{"code":-32603,"message":"unserializable"}}"#)
});
match framing {
Framing::Lines => writeln!(output, "{body}")?,
Framing::Headers => write!(output, "Content-Length: {}\r\n\r\n{body}", body.len())?,
}
output.flush()
}
#[cfg(test)]
mod tests {
use super::*;
fn read_all(input: &str, framing: Framing) -> Vec<String> {
let mut cursor = std::io::BufReader::new(input.as_bytes());
let mut out = Vec::new();
while let Ok(Some(message)) = read(&mut cursor, framing) {
out.push(message);
}
out
}
#[test]
fn reads_a_header_framed_message() {
let body = r#"{"jsonrpc":"2.0","id":1,"method":"initialize"}"#;
let wire = format!("Content-Length: {}\r\n\r\n{body}", body.len());
assert_eq!(read_all(&wire, Framing::Headers), [body]);
}
#[test]
fn reads_several_header_framed_messages() {
let a = r#"{"id":1}"#;
let b = r#"{"id":2}"#;
let wire = format!(
"Content-Length: {}\r\n\r\n{a}Content-Length: {}\r\n\r\n{b}",
a.len(),
b.len()
);
assert_eq!(read_all(&wire, Framing::Headers), [a, b]);
}
#[test]
fn the_header_name_is_case_insensitive() {
let body = r#"{"id":1}"#;
let wire = format!("content-length: {}\r\n\r\n{body}", body.len());
assert_eq!(read_all(&wire, Framing::Headers), [body]);
}
#[test]
fn other_headers_are_ignored() {
let body = r#"{"id":1}"#;
let wire = format!(
"Content-Type: application/vscode-jsonrpc\r\nContent-Length: {}\r\n\r\n{body}",
body.len()
);
assert_eq!(read_all(&wire, Framing::Headers), [body]);
}
#[test]
fn a_body_with_no_length_ends_the_stream() {
assert!(read_all("Content-Type: x\r\n\r\n{}", Framing::Headers).is_empty());
}
#[test]
fn reads_line_framed_messages() {
let wire = "{\"id\":1}\n{\"id\":2}\n";
assert_eq!(
read_all(wire, Framing::Lines),
[r#"{"id":1}"#, r#"{"id":2}"#]
);
}
#[test]
fn blank_lines_between_messages_are_skipped() {
let wire = "{\"id\":1}\n\n\n{\"id\":2}\n";
assert_eq!(
read_all(wire, Framing::Lines),
[r#"{"id":1}"#, r#"{"id":2}"#]
);
}
#[test]
fn empty_input_reads_nothing() {
assert!(read_all("", Framing::Headers).is_empty());
assert!(read_all("", Framing::Lines).is_empty());
}
#[test]
fn a_notification_expects_no_reply() {
let notification: Incoming =
serde_json::from_str(r#"{"method":"initialized","params":{}}"#).expect("parses");
assert!(!notification.expects_reply());
let request: Incoming =
serde_json::from_str(r#"{"id":1,"method":"initialize"}"#).expect("parses");
assert!(request.expects_reply());
}
#[test]
fn params_default_to_null_when_absent() {
let message: Incoming =
serde_json::from_str(r#"{"id":1,"method":"shutdown"}"#).expect("parses");
assert!(message.params.is_null());
}
#[test]
fn a_written_message_round_trips_through_the_reader() {
for framing in [Framing::Headers, Framing::Lines] {
let mut buffer = Vec::new();
write(
&mut buffer,
framing,
&Outgoing::result(Some(Value::from(7)), serde_json::json!({"ok": true})),
)
.expect("writes");
let text = String::from_utf8(buffer).expect("utf-8");
let read_back = read_all(&text, framing);
assert_eq!(read_back.len(), 1, "{framing:?}");
let parsed: Value = serde_json::from_str(&read_back[0]).expect("parses");
assert_eq!(parsed["id"], 7, "{framing:?}");
assert_eq!(parsed["result"]["ok"], true, "{framing:?}");
assert_eq!(parsed["jsonrpc"], "2.0", "{framing:?}");
}
}
#[test]
fn a_header_framed_write_states_the_byte_length_not_the_character_count() {
let mut buffer = Vec::new();
write(
&mut buffer,
Framing::Headers,
&Outgoing::result(None, serde_json::json!({"m": "café — ✓"})),
)
.expect("writes");
let text = String::from_utf8(buffer).expect("utf-8");
let (header, body) = text.split_once("\r\n\r\n").expect("framed");
let declared: usize = header
.trim_start_matches("Content-Length:")
.trim()
.parse()
.expect("a number");
assert_eq!(declared, body.len());
assert_ne!(
declared,
body.chars().count(),
"the test needs a multi-byte body"
);
}
#[test]
fn an_error_reply_carries_a_code_and_no_result() {
let message = Outgoing::error(Some(Value::from(1)), codes::METHOD_NOT_FOUND, "nope");
let rendered = serde_json::to_value(&message).expect("serializes");
assert_eq!(rendered["error"]["code"], codes::METHOD_NOT_FOUND);
assert!(rendered.get("result").is_none());
}
#[test]
fn a_notification_carries_a_method_and_no_id() {
let message = Outgoing::notification("textDocument/publishDiagnostics", Value::Null);
let rendered = serde_json::to_value(&message).expect("serializes");
assert_eq!(rendered["method"], "textDocument/publishDiagnostics");
assert!(rendered.get("id").is_none());
}
}