use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Request {
pub id: u64,
pub verb: String,
#[serde(default)]
pub args: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Response {
pub id: u64,
#[serde(flatten)]
pub outcome: ResponseOutcome,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ResponseOutcome {
Result { result: serde_json::Value },
Error { error: WireError },
Event { event: serde_json::Value },
End { end: EndMarker },
}
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct EndMarker {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WireError {
pub kind: WireErrorKind,
pub message: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl WireError {
#[must_use]
pub fn new(kind: WireErrorKind, message: impl Into<String>) -> Self {
Self { kind, message: message.into(), details: None }
}
#[must_use]
pub fn with_details(mut self, details: serde_json::Value) -> Self {
self.details = Some(details);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WireErrorKind {
UnknownVerb,
BadArgs,
Internal,
Timeout,
NotImplemented,
}
pub fn encode_line<T: Serialize>(value: &T) -> Result<Vec<u8>, serde_json::Error> {
let mut buf = serde_json::to_vec(value)?;
buf.push(b'\n');
Ok(buf)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_round_trips_through_json_with_args() {
let req =
Request { id: 42, verb: "stats".to_string(), args: serde_json::json!({ "scope": "all" }) };
let encoded = serde_json::to_string(&req).expect("serialize");
let decoded: Request = serde_json::from_str(&encoded).expect("deserialize");
assert_eq!(decoded.id, 42);
assert_eq!(decoded.verb, "stats");
assert_eq!(decoded.args, serde_json::json!({ "scope": "all" }));
}
#[test]
fn request_default_args_are_null() {
let raw = r#"{"id":1,"verb":"ping"}"#;
let req: Request = serde_json::from_str(raw).expect("deserialize");
assert!(req.args.is_null());
}
#[test]
fn response_result_serializes_with_flat_result_key() {
let resp = Response {
id: 7,
outcome: ResponseOutcome::Result { result: serde_json::json!({ "pong": true }) },
};
let value = serde_json::to_value(&resp).expect("to_value");
assert_eq!(value["id"], 7);
assert_eq!(value["result"], serde_json::json!({ "pong": true }));
assert!(value.get("error").is_none(), "result frame must not carry error key");
assert!(value.get("outcome").is_none(), "must flatten — no nested outcome key");
}
#[test]
fn response_error_serializes_with_flat_error_key() {
let resp = Response {
id: 3,
outcome: ResponseOutcome::Error {
error: WireError::new(WireErrorKind::UnknownVerb, "no such verb"),
},
};
let value = serde_json::to_value(&resp).expect("to_value");
assert_eq!(value["id"], 3);
assert_eq!(value["error"]["kind"], "unknown_verb");
assert_eq!(value["error"]["message"], "no such verb");
assert!(value.get("result").is_none());
assert!(value["error"].get("details").is_none(), "details omitted when None");
}
#[test]
fn wire_error_with_details_round_trips() {
let err = WireError::new(WireErrorKind::BadArgs, "compile failed")
.with_details(serde_json::json!({"file": "rules/a.json", "line": 12}));
let s = serde_json::to_string(&err).expect("serialize");
assert!(s.contains("\"details\""), "details present on the wire: {s}");
let back: WireError = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back.kind, WireErrorKind::BadArgs);
assert_eq!(back.details.expect("details")["line"], 12);
}
#[test]
fn unknown_verb_kind_round_trips_via_snake_case() {
for kind in [
WireErrorKind::UnknownVerb,
WireErrorKind::BadArgs,
WireErrorKind::Internal,
WireErrorKind::Timeout,
WireErrorKind::NotImplemented,
] {
let s = serde_json::to_string(&kind).expect("serialize kind");
let back: WireErrorKind = serde_json::from_str(&s).expect("deserialize kind");
assert_eq!(kind, back);
}
assert_eq!(serde_json::to_string(&WireErrorKind::UnknownVerb).unwrap(), "\"unknown_verb\"");
assert_eq!(serde_json::to_string(&WireErrorKind::BadArgs).unwrap(), "\"bad_args\"");
assert_eq!(serde_json::to_string(&WireErrorKind::Timeout).unwrap(), "\"timeout\"");
assert_eq!(
serde_json::to_string(&WireErrorKind::NotImplemented).unwrap(),
"\"not_implemented\""
);
}
#[test]
fn response_event_outcome_serializes_with_event_key() {
let resp = Response {
id: 9,
outcome: ResponseOutcome::Event { event: serde_json::json!({ "kind": "trajectory" }) },
};
let value = serde_json::to_value(&resp).expect("to_value");
assert_eq!(value["id"], 9);
assert_eq!(value["event"]["kind"], "trajectory");
assert!(value.get("result").is_none());
assert!(value.get("error").is_none());
assert!(value.get("end").is_none());
}
#[test]
fn response_end_outcome_serializes_as_empty_end_object() {
let resp = Response { id: 4, outcome: ResponseOutcome::End { end: EndMarker {} } };
let value = serde_json::to_value(&resp).expect("to_value");
assert_eq!(value["id"], 4);
assert_eq!(value["end"], serde_json::json!({}));
assert!(value.get("event").is_none());
}
#[test]
fn response_event_round_trips_through_json() {
let frames = vec![
Response { id: 1, outcome: ResponseOutcome::Result { result: serde_json::json!(42) } },
Response { id: 2, outcome: ResponseOutcome::Event { event: serde_json::json!("hi") } },
Response { id: 3, outcome: ResponseOutcome::End { end: EndMarker {} } },
];
for f in frames {
let s = serde_json::to_string(&f).expect("serialize");
let back: Response = serde_json::from_str(&s).expect("deserialize");
assert_eq!(back.id, f.id);
match (&f.outcome, &back.outcome) {
(ResponseOutcome::Result { .. }, ResponseOutcome::Result { .. })
| (ResponseOutcome::Event { .. }, ResponseOutcome::Event { .. })
| (ResponseOutcome::End { .. }, ResponseOutcome::End { .. }) => {}
other => panic!("variant changed: {other:?}"),
}
}
}
#[test]
fn encode_line_appends_newline() {
let req = Request { id: 1, verb: "ping".to_string(), args: serde_json::Value::Null };
let bytes = encode_line(&req).expect("encode");
assert_eq!(*bytes.last().expect("non-empty"), b'\n');
let body = &bytes[..bytes.len() - 1];
let decoded: Request = serde_json::from_slice(body).expect("decode body");
assert_eq!(decoded.verb, "ping");
}
}