use serde::{Deserialize, Serialize};
use serde_json::Value as JsonValue;
use super::error::ProtocolError;
pub const AXON_WIRE_VERSION: u8 = 1;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "lowercase")]
pub enum Frame {
Send {
#[serde(rename = "payload_type")]
payload_type: String,
data: JsonValue,
},
Select {
label: String,
},
End,
Error {
code: String,
detail: String,
},
}
impl Frame {
pub fn kind_tag(&self) -> &'static str {
match self {
Frame::Send { .. } => "send",
Frame::Select { .. } => "select",
Frame::End => "end",
Frame::Error { .. } => "error",
}
}
pub fn to_wire(&self) -> String {
let inner = serde_json::to_string(self).expect("Frame ⇒ JSON is total");
debug_assert!(inner.starts_with('{') && inner.ends_with('}'));
if inner == "{}" {
return format!("{{\"v\":{AXON_WIRE_VERSION}}}");
}
format!("{{\"v\":{AXON_WIRE_VERSION},{}", &inner[1..])
}
pub fn from_wire(s: &str) -> Result<Frame, ProtocolError> {
let raw: JsonValue = serde_json::from_str(s)
.map_err(|e| ProtocolError::MalformedFrame(format!("invalid JSON: {e}")))?;
let obj = raw
.as_object()
.ok_or_else(|| ProtocolError::MalformedFrame("envelope is not a JSON object".into()))?;
let v = obj
.get("v")
.and_then(JsonValue::as_u64)
.ok_or_else(|| ProtocolError::MalformedFrame("missing wire-version `v`".into()))?;
if v != AXON_WIRE_VERSION as u64 {
return Err(ProtocolError::MalformedFrame(format!(
"unsupported wire version {v} (this runtime speaks v{AXON_WIRE_VERSION})"
)));
}
let mut inner = obj.clone();
inner.remove("v");
serde_json::from_value::<Frame>(JsonValue::Object(inner)).map_err(|e| {
ProtocolError::MalformedFrame(format!(
"unknown frame kind or missing field: {e}"
))
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn send_frame_round_trips_through_wire() {
let f = Frame::Send {
payload_type: "Msg".into(),
data: json!({"text": "hello"}),
};
let wire = f.to_wire();
assert!(wire.starts_with("{\"v\":1,"));
let parsed = Frame::from_wire(&wire).expect("parse");
assert_eq!(parsed, f);
}
#[test]
fn select_frame_round_trips() {
let f = Frame::Select { label: "ask".into() };
assert_eq!(Frame::from_wire(&f.to_wire()).unwrap(), f);
}
#[test]
fn end_frame_is_a_bare_kind_marker() {
let wire = Frame::End.to_wire();
assert_eq!(wire, "{\"v\":1,\"kind\":\"end\"}");
assert_eq!(Frame::from_wire(&wire).unwrap(), Frame::End);
}
#[test]
fn error_frame_round_trips_with_code_and_detail() {
let f = Frame::Error {
code: "credit_exhausted".into(),
detail: "n=0 on send Msg".into(),
};
assert_eq!(Frame::from_wire(&f.to_wire()).unwrap(), f);
}
#[test]
fn missing_version_is_malformed() {
let s = r#"{"kind":"end"}"#;
match Frame::from_wire(s) {
Err(ProtocolError::MalformedFrame(m)) => assert!(m.contains("wire-version")),
other => panic!("expected MalformedFrame, got {other:?}"),
}
}
#[test]
fn unsupported_version_is_malformed() {
let s = r#"{"v":99,"kind":"end"}"#;
match Frame::from_wire(s) {
Err(ProtocolError::MalformedFrame(m)) => assert!(m.contains("unsupported wire version")),
other => panic!("expected version-mismatch malformed, got {other:?}"),
}
}
#[test]
fn unknown_kind_is_malformed() {
let s = r#"{"v":1,"kind":"yeet","payload_type":"X"}"#;
assert!(matches!(Frame::from_wire(s), Err(ProtocolError::MalformedFrame(_))));
}
#[test]
fn invalid_json_is_malformed() {
let s = r#"{"v":1,"kind":"send""#; assert!(matches!(Frame::from_wire(s), Err(ProtocolError::MalformedFrame(_))));
}
#[test]
fn non_object_envelope_is_malformed() {
let s = "[1,2,3]";
assert!(matches!(Frame::from_wire(s), Err(ProtocolError::MalformedFrame(_))));
}
#[test]
fn kind_tag_matches_wire_tag() {
let cases = [
(Frame::Send { payload_type: "T".into(), data: json!(null) }, "send"),
(Frame::Select { label: "a".into() }, "select"),
(Frame::End, "end"),
(Frame::Error { code: "c".into(), detail: "d".into() }, "error"),
];
for (f, tag) in cases {
assert_eq!(f.kind_tag(), tag);
}
}
}