//! Defensive, pure parsers retained for the explicit test-only server contract. //! //! SITE-01 contains production project transport by omission: this module has //! no URL configuration, credentials, request constructors, HTTP dispatch, //! response callback, or store mutation. Secure authenticated transport must //! be introduced from scratch under SITE-12 gates. /// One text line out of a timeline event, or `None` for non-message events. /// Shapes handled: `content.Text.body`, `content.body`, and bare `body`. /// Anything else is skipped rather than guessed. pub fn event_text(event: &serde_json::Value) -> Option<(String, String)> { if event.get("type").and_then(|value| value.as_str()) != Some("message") { return None; } let body = event .get("content") .and_then(|content| { content .get("Text") .and_then(|text| text.get("body")) .or_else(|| content.get("body")) }) .or_else(|| event.get("body")) .and_then(|body| body.as_str())?; let sender = event .get("sender") .and_then(|sender| sender.as_str()) .unwrap_or("?") .to_string(); Some((sender, body.to_string())) } /// Extract timeline bodies defensively for the ignored interoperability test. pub fn timeline_bodies(payload: &str) -> Vec { let Ok(value) = serde_json::from_str::(payload) else { return Vec::new(); }; let Some(events) = value.get("events").and_then(|events| events.as_array()) else { return Vec::new(); }; events .iter() .filter_map(|event| event_text(event).map(|(_, body)| body)) .collect() } /// Parse a test-server `/sync` fixture into per-room message lines. /// Unknown or malformed shapes return an empty collection and never panic. pub fn sync_timelines(payload: &str) -> Vec<(String, Vec)> { let Ok(value) = serde_json::from_str::(payload) else { return Vec::new(); }; let Some(rooms) = value.get("joined_rooms").and_then(|rooms| rooms.as_array()) else { return Vec::new(); }; rooms .iter() .filter_map(|room| { let room_id = room.get("room_id").and_then(|id| id.as_str())?; let timeline = room .get("timeline") .and_then(|timeline| timeline.as_array())?; let lines: Vec = timeline .iter() .filter_map(|event| { event_text(event).map(|(sender, body)| format!("{sender}: {body}")) }) .collect(); (!lines.is_empty()).then(|| (room_id.to_string(), lines)) }) .collect() } #[cfg(test)] mod tests { use super::*; #[test] fn timeline_parsers_ignore_malformed_and_non_message_content() { let payload = serde_json::json!({ "joined_rooms": [ { "room_id": "room_abc123", "timeline": [ {"type": "message", "sender": "alice", "content": {"Text": {"body": "slab done", "formatted_body": null, "mentions": []}}}, {"type": "membership", "sender": "alice"}, {"type": "message", "sender": "bob", "content": {"body": "noted"}}, {"type": "message", "sender": "x", "content": {"Image": {"body": "pic"}}} ] }, {"room_id": "room_empty", "timeline": []} ] }) .to_string(); let timelines = sync_timelines(&payload); assert_eq!(timelines.len(), 1); assert_eq!(timelines[0].0, "room_abc123"); assert_eq!( timelines[0].1, vec!["alice: slab done".to_string(), "bob: noted".to_string()] ); assert!(sync_timelines("{}").is_empty()); assert!(sync_timelines("garbage").is_empty()); assert!(timeline_bodies("garbage").is_empty()); } }