use std::io::{self, BufRead, Read, Write};
use serde_json::{Map, Value, json};
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Incoming {
Request {
id: Value,
method: String,
params: Map<String, Value>,
},
Notification {
method: String,
params: Map<String, Value>,
},
}
#[derive(Debug, thiserror::Error, PartialEq, Eq)]
pub enum FrameError {
#[error("a line over {limit} bytes was refused")]
Overlong {
limit: usize,
},
#[error("the line is not JSON: {message}")]
NotJson {
message: String,
},
#[error("the line is not a JSON-RPC 2.0 request: {message}")]
NotRequest {
message: &'static str,
},
#[error("the host stream failed: {message}")]
Stream {
message: String,
},
}
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 UNSUPPORTED_PROTOCOL_VERSION: i64 = -32022;
pub fn read_message<R: BufRead>(
reader: &mut R,
limit: usize,
) -> Result<Option<Incoming>, FrameError> {
let mut line = Vec::new();
let read = Read::by_ref(reader)
.take(limit as u64 + 1)
.read_until(b'\n', &mut line)
.map_err(|e| FrameError::Stream {
message: e.to_string(),
})?;
if read == 0 {
return Ok(None);
}
if line.len() > limit {
drain_to_newline(reader)?;
return Err(FrameError::Overlong { limit });
}
let text = String::from_utf8_lossy(&line);
let text = text.trim_end_matches(['\r', '\n']);
if text.trim().is_empty() {
return read_message(reader, limit);
}
let value: Value = serde_json::from_str(text).map_err(|e| FrameError::NotJson {
message: e.to_string(),
})?;
parse(value).map(Some)
}
fn drain_to_newline<R: BufRead>(reader: &mut R) -> Result<(), FrameError> {
let mut scratch = Vec::new();
loop {
scratch.clear();
let read = Read::by_ref(reader)
.take(4096)
.read_until(b'\n', &mut scratch)
.map_err(|e| FrameError::Stream {
message: e.to_string(),
})?;
if read == 0 || scratch.last() == Some(&b'\n') {
return Ok(());
}
}
}
pub fn parse(value: Value) -> Result<Incoming, FrameError> {
let Value::Object(mut object) = value else {
return Err(FrameError::NotRequest {
message: "not an object",
});
};
if object.get("jsonrpc").and_then(Value::as_str) != Some("2.0") {
return Err(FrameError::NotRequest {
message: "jsonrpc is not \"2.0\"",
});
}
let Some(method) = object
.get("method")
.and_then(Value::as_str)
.map(str::to_owned)
else {
return Err(FrameError::NotRequest {
message: "method is missing or not a string",
});
};
let params = match object.remove("params") {
None | Some(Value::Null) => Map::new(),
Some(Value::Object(params)) => params,
Some(_) => {
return Err(FrameError::NotRequest {
message: "params is not an object",
});
}
};
match object.remove("id") {
None => Ok(Incoming::Notification { method, params }),
Some(Value::Null) => Err(FrameError::NotRequest {
message: "id is null",
}),
Some(id @ (Value::String(_) | Value::Number(_))) => {
Ok(Incoming::Request { id, method, params })
}
Some(_) => Err(FrameError::NotRequest {
message: "id is not a string or a number",
}),
}
}
#[must_use]
pub fn result(id: &Value, result: &Value) -> Value {
json!({ "jsonrpc": "2.0", "id": id, "result": result })
}
#[must_use]
pub fn error(id: &Value, code: i64, message: &str, data: Option<Value>) -> Value {
let mut error = json!({ "code": code, "message": message });
if let (Some(data), Some(object)) = (data, error.as_object_mut()) {
object.insert("data".to_owned(), data);
}
json!({ "jsonrpc": "2.0", "id": id, "error": error })
}
pub fn write_message<W: Write>(writer: &mut W, message: &Value) -> io::Result<()> {
let mut line = serde_json::to_vec(message)?;
line.push(b'\n');
writer.write_all(&line)?;
writer.flush()
}
#[cfg(test)]
#[path = "framing_tests.rs"]
mod tests;