diff --git a/crates/xy-ipc/src/envelope.rs b/crates/xy-ipc/src/envelope.rs new file mode 100644 index 0000000..fa7d579 --- /dev/null +++ b/crates/xy-ipc/src/envelope.rs @@ -0,0 +1,114 @@ +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Request { + pub jsonrpc: String, + pub id: serde_json::Value, + pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Notification { + pub jsonrpc: String, + pub method: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub params: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Response { + pub jsonrpc: String, + pub id: serde_json::Value, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub result: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub error: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct RpcError { + pub code: i32, + pub message: String, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub data: Option, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +pub enum Incoming { + Request(Request), + Response(Response), + Notification(Notification), +} + +pub const JSONRPC_VERSION: &str = "2.0"; + +pub fn request(id: u64, method: &str, params: Option) -> Request { + Request { + jsonrpc: JSONRPC_VERSION.into(), + id: serde_json::json!(id), + method: method.into(), + params, + } +} + +pub fn notification(method: &str, params: Option) -> Notification { + Notification { + jsonrpc: JSONRPC_VERSION.into(), + method: method.into(), + params, + } +} + +pub fn ok_response(id: serde_json::Value, result: Value) -> Response { + Response { + jsonrpc: JSONRPC_VERSION.into(), + id, + result: Some(result), + error: None, + } +} + +pub fn err_response(id: serde_json::Value, code: i32, message: String) -> Response { + Response { + jsonrpc: JSONRPC_VERSION.into(), + id, + result: None, + error: Some(RpcError { + code, + message, + data: None, + }), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn request_round_trip() { + let r = request(7, "list", None); + let s = serde_json::to_string(&r).unwrap(); + let back: Request = serde_json::from_str(&s).unwrap(); + assert_eq!(back.method, "list"); + assert_eq!(back.id, serde_json::json!(7)); + } + + #[test] + fn incoming_is_response() { + let s = r#"{"jsonrpc":"2.0","id":1,"result":{"ok":true}}"#; + let i: Incoming = serde_json::from_str(s).unwrap(); + assert!(matches!(i, Incoming::Response(_))); + } + + #[test] + fn incoming_is_notification() { + let s = r#"{"jsonrpc":"2.0","method":"log","params":{}}"#; + let i: Incoming = serde_json::from_str(s).unwrap(); + assert!(matches!(i, Incoming::Notification(_))); + } +} diff --git a/crates/xy-ipc/src/lib.rs b/crates/xy-ipc/src/lib.rs index 2ad7623..59c3f78 100644 --- a/crates/xy-ipc/src/lib.rs +++ b/crates/xy-ipc/src/lib.rs @@ -1 +1,8 @@ //! JSON-RPC 2.0 over newline-delimited JSON on a Unix socket. + +pub mod envelope; + +pub use envelope::{ + err_response, notification, ok_response, request, Incoming, Notification, Request, Response, + RpcError, +};