use std::collections::HashMap;
use jmap_types::{Id, State};
use crate::push;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct SseFrame {
pub event: SseEvent,
pub id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum SseEvent {
StateChange(push::StateChange),
Unknown { event_type: String },
}
pub fn parse_sse_block(block: &str) -> SseFrame {
let mut event_type: Option<&str> = None;
let mut data_lines: Vec<&str> = Vec::new();
let mut id: Option<String> = None;
for line in block.lines() {
if let Some(value) = line.strip_prefix("event:") {
event_type = Some(value.strip_prefix(' ').unwrap_or(value));
} else if let Some(value) = line.strip_prefix("data:") {
data_lines.push(value.strip_prefix(' ').unwrap_or(value));
} else if let Some(value) = line.strip_prefix("id:") {
let v = value.strip_prefix(' ').unwrap_or(value);
id = if v.is_empty() {
None
} else {
Some(v.to_owned())
};
}
}
let event = match event_type {
Some("state") => match data_lines.as_slice() {
[] => SseEvent::Unknown {
event_type: "state".to_owned(),
}, [single] => parse_state_data("state", single),
_ => parse_state_data("state", &data_lines.join("\n")),
},
Some(t) => SseEvent::Unknown {
event_type: t.to_owned(),
},
None => SseEvent::Unknown {
event_type: String::new(),
},
};
SseFrame { event, id }
}
fn parse_state_data(event_type: &str, data: &str) -> SseEvent {
match try_parse_state_change(data) {
Some(sc) => SseEvent::StateChange(sc),
None => SseEvent::Unknown {
event_type: event_type.to_owned(),
},
}
}
fn try_parse_state_change(data: &str) -> Option<push::StateChange> {
let mut v = serde_json::from_str::<serde_json::Value>(data).ok()?;
let obj = v.as_object_mut()?;
let changed_val = obj.remove("changed")?;
let changed =
serde_json::from_value::<HashMap<Id, HashMap<String, State>>>(changed_val).ok()?;
Some(push::StateChange { changed })
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_state_event() {
let block = "event: state\ndata: {\"changed\":{\"acc1\":{\"Message\":\"s42\"}}}";
let SseFrame { event, .. } = parse_sse_block(block);
match event {
SseEvent::StateChange(sc) => {
assert_eq!(
sc.changed
.get("acc1")
.and_then(|m| m.get("Message"))
.map(|s| s.as_ref()),
Some("s42"),
"changed[acc1][Message] must equal s42"
);
}
other => panic!("expected StateChange, got {other:?}"),
}
}
#[test]
fn parse_state_event_with_type_field() {
let block = "event: state\ndata: {\"@type\":\"StateChange\",\"changed\":{\"acc1\":{\"Message\":\"s42\"}}}";
let SseFrame { event, .. } = parse_sse_block(block);
match event {
SseEvent::StateChange(sc) => {
assert_eq!(
sc.changed
.get("acc1")
.and_then(|m| m.get("Message"))
.map(|s| s.as_ref()),
Some("s42"),
"changed[acc1][Message] must equal s42"
);
}
other => panic!("expected StateChange, got {other:?}"),
}
}
#[test]
fn parse_unknown_event() {
let block = "event: ping\ndata: {}";
let SseFrame { event, .. } = parse_sse_block(block);
assert!(
matches!(event, SseEvent::Unknown { .. }),
"unrecognized event type must yield Unknown"
);
}
#[test]
fn parse_empty_block() {
let SseFrame { event, id } = parse_sse_block("");
assert!(
matches!(event, SseEvent::Unknown { .. }),
"empty block must yield Unknown"
);
assert!(id.is_none(), "empty block must have no id");
}
#[test]
fn parse_malformed_data_json() {
let block = "event: state\ndata: not-json";
let SseFrame { event, .. } = parse_sse_block(block);
assert!(
matches!(event, SseEvent::Unknown { .. }),
"malformed JSON must yield Unknown, not panic or error"
);
}
#[test]
fn parse_id_line() {
let block = "id: evt-42\nevent: state\ndata: {\"changed\":{}}";
let SseFrame { event, id } = parse_sse_block(block);
assert_eq!(id.as_deref(), Some("evt-42"), "id must be evt-42");
assert!(
matches!(event, SseEvent::StateChange(_)),
"must still parse as StateChange"
);
}
#[test]
fn parse_multiline_data() {
let block = concat!(
"event: state\n",
"data: {\"changed\":{\"acc1\":{\"Message\":\"s1\"}}}\n",
"data: extra"
);
let SseFrame { event, .. } = parse_sse_block(block);
assert!(
matches!(event, SseEvent::Unknown { .. }),
"both data: lines must be joined: first-line-valid JSON + second line = Unknown"
);
}
#[test]
fn sse_event_no_typing_or_presence() {
let e = SseEvent::Unknown {
event_type: String::new(),
};
match e {
SseEvent::StateChange(_) => {}
SseEvent::Unknown { .. } => {}
}
}
}