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,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum WireErrorKind {
UnknownVerb,
BadArgs,
Internal,
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 { kind: WireErrorKind::UnknownVerb, message: "no such verb".to_string() },
},
};
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());
}
#[test]
fn unknown_verb_kind_round_trips_via_snake_case() {
for kind in [
WireErrorKind::UnknownVerb,
WireErrorKind::BadArgs,
WireErrorKind::Internal,
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::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");
}
}