toddy_core/protocol/
mod.rs1mod incoming;
13mod outgoing;
14mod types;
15
16pub const PROTOCOL_VERSION: u32 = 1;
19
20pub use incoming::{ExtensionCommandItem, IncomingMessage};
21pub use outgoing::{
22 EffectResponse, InteractResponse, KeyModifiers, OutgoingEvent, QueryResponse, ResetResponse,
23 TreeHashResponse,
24};
25pub use types::{PatchOp, TreeNode};
26
27#[derive(Debug)]
34pub struct SessionMessage {
35 pub session: String,
36 pub message: IncomingMessage,
37}
38
39impl SessionMessage {
40 pub fn from_value(mut value: serde_json::Value) -> Result<Self, serde_json::Error> {
46 let session = value
47 .as_object_mut()
48 .and_then(|obj| obj.remove("session"))
49 .and_then(|v| v.as_str().map(String::from))
50 .unwrap_or_default();
51
52 let message = serde_json::from_value(value)?;
53 Ok(Self { session, message })
54 }
55}
56
57#[cfg(test)]
58mod tests {
59 use super::*;
60 use serde_json::json;
61
62 #[test]
63 fn session_message_extracts_session() {
64 let val = json!({
65 "session": "test_1",
66 "type": "snapshot",
67 "tree": {"id": "r", "type": "column", "props": {}, "children": []}
68 });
69 let sm = SessionMessage::from_value(val).unwrap();
70 assert_eq!(sm.session, "test_1");
71 assert!(matches!(sm.message, IncomingMessage::Snapshot { .. }));
72 }
73
74 #[test]
75 fn session_message_defaults_to_empty() {
76 let val = json!({
77 "type": "reset",
78 "id": "r1"
79 });
80 let sm = SessionMessage::from_value(val).unwrap();
81 assert_eq!(sm.session, "");
82 assert!(matches!(sm.message, IncomingMessage::Reset { .. }));
83 }
84
85 #[test]
86 fn session_message_preserves_all_fields() {
87 let val = json!({
88 "session": "s42",
89 "type": "query",
90 "id": "q1",
91 "target": "find",
92 "selector": {"id": "btn"}
93 });
94 let sm = SessionMessage::from_value(val).unwrap();
95 assert_eq!(sm.session, "s42");
96 match sm.message {
97 IncomingMessage::Query { id, target, .. } => {
98 assert_eq!(id, "q1");
99 assert_eq!(target, "find");
100 }
101 _ => panic!("expected Query"),
102 }
103 }
104
105 #[test]
106 fn outgoing_event_includes_session() {
107 let evt = OutgoingEvent::click("btn".to_string());
108 let json = serde_json::to_value(&evt).unwrap();
109 assert_eq!(json["session"], "");
110 }
111
112 #[test]
113 fn outgoing_event_with_session() {
114 let evt = OutgoingEvent::click("btn".to_string()).with_session("s1".to_string());
115 let json = serde_json::to_value(&evt).unwrap();
116 assert_eq!(json["session"], "s1");
117 }
118
119 #[test]
120 fn effect_response_includes_session() {
121 let resp =
122 EffectResponse::ok("e1".to_string(), json!("data")).with_session("s2".to_string());
123 let json = serde_json::to_value(&resp).unwrap();
124 assert_eq!(json["session"], "s2");
125 }
126
127 #[test]
128 fn reset_response_includes_session() {
129 let resp = ResetResponse::ok("r1".to_string()).with_session("s3");
130 let json = serde_json::to_value(&resp).unwrap();
131 assert_eq!(json["session"], "s3");
132 }
133
134 #[test]
135 fn interact_response_propagates_session_to_events() {
136 let events = vec![
137 OutgoingEvent::click("btn".to_string()),
138 OutgoingEvent::input("inp".to_string(), "text".to_string()),
139 ];
140 let resp = InteractResponse::new("i1".to_string(), events).with_session("s4");
141 let json = serde_json::to_value(&resp).unwrap();
142 assert_eq!(json["session"], "s4");
143 assert_eq!(json["events"][0]["session"], "s4");
144 assert_eq!(json["events"][1]["session"], "s4");
145 }
146
147 #[test]
148 fn session_message_rejects_non_object() {
149 let val = json!([1, 2, 3]);
150 let result = SessionMessage::from_value(val);
151 assert!(result.is_err());
152 }
153
154 #[test]
155 fn session_message_ignores_non_string_session() {
156 let val = json!({
157 "session": 42,
158 "type": "reset",
159 "id": "r1"
160 });
161 let sm = SessionMessage::from_value(val).unwrap();
162 assert_eq!(sm.session, "");
163 }
164}