use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, PartialEq)]
pub(crate) struct ServerEnvelope {
pub ch: String,
pub seq: u64,
#[serde(flatten)]
pub body: EnvelopeBody,
}
#[derive(Debug, Clone, Serialize, PartialEq)]
#[serde(untagged)]
pub(crate) enum EnvelopeBody {
Event {
event: Value,
},
Control {
control: Value,
},
}
impl ServerEnvelope {
pub(crate) fn event(ch: impl Into<String>, seq: u64, event: Value) -> Self {
Self {
ch: ch.into(),
seq,
body: EnvelopeBody::Event { event },
}
}
pub(crate) fn control(ch: impl Into<String>, seq: u64, control: Value) -> Self {
Self {
ch: ch.into(),
seq,
body: EnvelopeBody::Control { control },
}
}
pub(crate) fn to_text(&self) -> Option<String> {
serde_json::to_string(self).ok()
}
}
pub(crate) fn terminal_control(reason: &str) -> Value {
serde_json::json!({ "type": "terminal", "reason": reason })
}
pub(crate) fn feed_reset_control(from_seq: u64) -> Value {
serde_json::json!({ "type": "feed_reset", "from_seq": from_seq })
}
#[derive(Debug, Clone, Deserialize, PartialEq)]
#[serde(tag = "type", rename_all = "snake_case")]
pub(crate) enum ClientFrame {
Hello {
#[serde(default)]
device_id: Option<String>,
#[serde(default)]
token: Option<String>,
},
Subscribe {
ch: String,
#[serde(default)]
since: Option<u64>,
},
Unsubscribe { ch: String },
Stop { session_id: String },
#[serde(other)]
Unknown,
}
#[derive(Debug, Clone, PartialEq)]
pub(crate) enum Channel {
Feed,
Agent(String),
}
impl Channel {
pub(crate) fn parse(ch: &str) -> Option<Channel> {
if ch == "feed" {
Some(Channel::Feed)
} else if let Some(sid) = ch.strip_prefix("agent.") {
if sid.is_empty() {
None
} else {
Some(Channel::Agent(sid.to_string()))
}
} else {
None
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn server_envelope_event_shape() {
let env = ServerEnvelope::event(
"agent.sess_abc",
42,
json!({ "type": "token", "content": "Hello" }),
);
let v = serde_json::to_value(&env).unwrap();
assert_eq!(
v,
json!({
"ch": "agent.sess_abc",
"seq": 42,
"event": { "type": "token", "content": "Hello" }
})
);
assert_eq!(v.as_object().unwrap().len(), 3);
}
#[test]
fn server_envelope_control_shape() {
let env = ServerEnvelope::control("agent.sess_abc", 43, terminal_control("complete"));
let v = serde_json::to_value(&env).unwrap();
assert_eq!(
v,
json!({
"ch": "agent.sess_abc",
"seq": 43,
"control": { "type": "terminal", "reason": "complete" }
})
);
}
#[test]
fn feed_reset_control_shape() {
let env = ServerEnvelope::control("feed", 0, feed_reset_control(1006));
let v = serde_json::to_value(&env).unwrap();
assert_eq!(
v["control"],
json!({ "type": "feed_reset", "from_seq": 1006 })
);
}
#[test]
fn client_frame_hello_parses() {
let f: ClientFrame =
serde_json::from_str(r#"{"type":"hello","device_id":"d1","token":"bd1_x"}"#).unwrap();
assert_eq!(
f,
ClientFrame::Hello {
device_id: Some("d1".to_string()),
token: Some("bd1_x".to_string())
}
);
let f: ClientFrame = serde_json::from_str(r#"{"type":"hello"}"#).unwrap();
assert_eq!(
f,
ClientFrame::Hello {
device_id: None,
token: None
}
);
}
#[test]
fn client_frame_subscribe_parses_with_and_without_cursor() {
let f: ClientFrame =
serde_json::from_str(r#"{"type":"subscribe","ch":"feed","since":1006}"#).unwrap();
assert_eq!(
f,
ClientFrame::Subscribe {
ch: "feed".to_string(),
since: Some(1006)
}
);
let f: ClientFrame =
serde_json::from_str(r#"{"type":"subscribe","ch":"agent.s1"}"#).unwrap();
assert_eq!(
f,
ClientFrame::Subscribe {
ch: "agent.s1".to_string(),
since: None
}
);
}
#[test]
fn client_frame_unsubscribe_and_stop_parse() {
let f: ClientFrame =
serde_json::from_str(r#"{"type":"unsubscribe","ch":"agent.s1"}"#).unwrap();
assert_eq!(
f,
ClientFrame::Unsubscribe {
ch: "agent.s1".to_string()
}
);
let f: ClientFrame = serde_json::from_str(r#"{"type":"stop","session_id":"s1"}"#).unwrap();
assert_eq!(
f,
ClientFrame::Stop {
session_id: "s1".to_string()
}
);
}
#[test]
fn unknown_frame_type_maps_to_unknown_not_error() {
let f: ClientFrame =
serde_json::from_str(r#"{"type":"execute","session_id":"s1","message":"hi"}"#).unwrap();
assert_eq!(f, ClientFrame::Unknown);
}
#[test]
fn malformed_json_is_a_parse_error_caught_by_driver() {
let r: Result<ClientFrame, _> = serde_json::from_str("not json{");
assert!(r.is_err());
let r: Result<ClientFrame, _> = serde_json::from_str(r#"{"ch":"feed"}"#);
assert!(r.is_err());
}
#[test]
fn channel_parse() {
assert_eq!(Channel::parse("feed"), Some(Channel::Feed));
assert_eq!(
Channel::parse("agent.sess_abc"),
Some(Channel::Agent("sess_abc".to_string()))
);
assert_eq!(Channel::parse("agent."), None);
assert_eq!(Channel::parse("bogus"), None);
}
}