use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RpcRequest<I> {
pub input: I,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RpcResponse<T, E> {
Ok {
ok: T,
},
Err {
err: E,
},
}
impl<T, E> RpcResponse<T, E> {
pub fn into_result(self) -> Result<T, E> {
match self {
RpcResponse::Ok { ok } => Ok(ok),
RpcResponse::Err { err } => Err(err),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ErrEnvelope<P> {
pub code: String,
pub payload: P,
}
impl<P> ErrEnvelope<P> {
pub fn new(code: impl Into<String>, payload: P) -> Self {
Self {
code: code.into(),
payload,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum SubFrame<T, E> {
Data(T),
Error(ErrEnvelope<E>),
End,
V {
v: u32,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", content = "payload", rename_all = "snake_case")]
pub enum WsMessage<T, E> {
Subscribe {
id: u64,
procedure: String,
input: serde_json::Value,
},
Unsubscribe {
id: u64,
},
Data {
id: u64,
value: T,
},
Error {
id: u64,
err: ErrEnvelope<E>,
},
End {
id: u64,
},
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn rpc_request_serialises_with_input_field() {
let req = RpcRequest { input: 42 };
let s = serde_json::to_string(&req).unwrap();
assert_eq!(s, r#"{"input":42}"#);
}
#[test]
fn rpc_response_ok_roundtrips() {
let resp: RpcResponse<u32, String> = RpcResponse::Ok { ok: 7 };
let s = serde_json::to_string(&resp).unwrap();
assert_eq!(s, r#"{"ok":7}"#);
let parsed: RpcResponse<u32, String> = serde_json::from_str(&s).unwrap();
match parsed {
RpcResponse::Ok { ok } => assert_eq!(ok, 7),
RpcResponse::Err { .. } => panic!("expected Ok"),
}
}
#[test]
fn rpc_response_err_roundtrips() {
let resp: RpcResponse<u32, String> = RpcResponse::Err {
err: "boom".to_string(),
};
let s = serde_json::to_string(&resp).unwrap();
assert_eq!(s, r#"{"err":"boom"}"#);
let parsed: RpcResponse<u32, String> = serde_json::from_str(&s).unwrap();
match parsed {
RpcResponse::Err { err } => assert_eq!(err, "boom"),
RpcResponse::Ok { .. } => panic!("expected Err"),
}
}
#[test]
fn rpc_response_into_result_collapses() {
let ok: RpcResponse<u32, String> = RpcResponse::Ok { ok: 1 };
assert_eq!(ok.into_result(), Ok(1));
let err: RpcResponse<u32, String> = RpcResponse::Err {
err: "x".to_string(),
};
assert_eq!(err.into_result(), Err("x".to_string()));
}
#[test]
fn err_envelope_new_builds_via_into() {
let e: ErrEnvelope<u32> = ErrEnvelope::new("not_found", 404);
assert_eq!(e.code, "not_found");
assert_eq!(e.payload, 404);
}
#[test]
fn sub_frame_data_serialises_with_payload() {
let f: SubFrame<u32, String> = SubFrame::Data(5);
let s = serde_json::to_string(&f).unwrap();
assert_eq!(s, r#"{"type":"data","payload":5}"#);
}
#[test]
fn sub_frame_error_serialises_envelope() {
let f: SubFrame<u32, String> = SubFrame::Error(ErrEnvelope::new("bad", "details".into()));
let v = serde_json::to_value(&f).unwrap();
assert_eq!(
v,
json!({
"type": "error",
"payload": { "code": "bad", "payload": "details" }
})
);
}
#[test]
fn sub_frame_end_serialises_without_payload_field() {
let f: SubFrame<u32, String> = SubFrame::End;
let s = serde_json::to_string(&f).unwrap();
assert_eq!(s, r#"{"type":"end"}"#);
let parsed: SubFrame<u32, String> = serde_json::from_str(&s).unwrap();
assert!(matches!(parsed, SubFrame::End));
let parsed: SubFrame<u32, String> =
serde_json::from_str(r#"{"type":"end","payload":null}"#).unwrap();
assert!(matches!(parsed, SubFrame::End));
}
#[test]
fn sub_frame_version_marker_roundtrips() {
let f: SubFrame<u32, String> = SubFrame::V { v: 1 };
let s = serde_json::to_string(&f).unwrap();
assert_eq!(s, r#"{"type":"v","payload":{"v":1}}"#);
let parsed: SubFrame<u32, String> = serde_json::from_str(&s).unwrap();
match parsed {
SubFrame::V { v } => assert_eq!(v, 1),
_ => panic!("expected V"),
}
}
#[test]
fn ws_message_subscribe_roundtrips() {
let m: WsMessage<u32, String> = WsMessage::Subscribe {
id: 42,
procedure: "user.events".to_string(),
input: json!({ "userId": 1 }),
};
let s = serde_json::to_string(&m).unwrap();
let v: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(
v,
json!({
"type": "subscribe",
"payload": {
"id": 42,
"procedure": "user.events",
"input": { "userId": 1 }
}
})
);
let parsed: WsMessage<u32, String> = serde_json::from_value(v).unwrap();
match parsed {
WsMessage::Subscribe {
id,
procedure,
input,
} => {
assert_eq!(id, 42);
assert_eq!(procedure, "user.events");
assert_eq!(input, json!({ "userId": 1 }));
}
_ => panic!("expected Subscribe"),
}
}
#[test]
fn ws_message_data_roundtrips() {
let m: WsMessage<u32, String> = WsMessage::Data { id: 1, value: 99 };
let s = serde_json::to_string(&m).unwrap();
let v: serde_json::Value = serde_json::from_str(&s).unwrap();
assert_eq!(
v,
json!({
"type": "data",
"payload": { "id": 1, "value": 99 }
})
);
let parsed: WsMessage<u32, String> = serde_json::from_value(v).unwrap();
match parsed {
WsMessage::Data { id, value } => {
assert_eq!(id, 1);
assert_eq!(value, 99);
}
_ => panic!("expected Data"),
}
}
}