1use serde::{Deserialize, Deserializer};
4use serde_json::Value;
5
6use super::types::{PatchOp, TreeNode};
7
8#[derive(Debug, Clone, Deserialize)]
10#[serde(tag = "type", rename_all = "snake_case")]
11pub enum IncomingMessage {
12 Snapshot { tree: TreeNode },
14 Patch { ops: Vec<PatchOp> },
16 Effect {
18 id: String,
19 kind: String,
20 payload: Value,
21 },
22 WidgetOp {
24 op: String,
25 #[serde(default)]
26 payload: Value,
27 },
28 Subscribe { kind: String, tag: String },
30 Unsubscribe { kind: String },
32 WindowOp {
34 op: String,
35 window_id: String,
36 #[serde(default)]
37 settings: Value,
38 },
39 Settings { settings: Value },
41 Query {
43 id: String,
44 target: String,
45 #[serde(default)]
46 selector: Value,
47 },
48 Interact {
50 id: String,
51 action: String,
52 #[serde(default)]
53 selector: Value,
54 #[serde(default)]
55 payload: Value,
56 },
57 #[allow(dead_code)]
61 TreeHash { id: String, name: String },
62 #[allow(dead_code)]
64 Screenshot {
65 id: String,
66 name: String,
67 #[serde(default)]
68 width: Option<u32>,
69 #[serde(default)]
70 height: Option<u32>,
71 },
72 Reset { id: String },
74 ImageOp {
79 op: String,
80 handle: String,
81 #[serde(default, deserialize_with = "deserialize_binary_field")]
82 data: Option<Vec<u8>>,
83 #[serde(default, deserialize_with = "deserialize_binary_field")]
84 pixels: Option<Vec<u8>>,
85 #[serde(default)]
86 width: Option<u32>,
87 #[serde(default)]
88 height: Option<u32>,
89 },
90 ExtensionCommand {
93 node_id: String,
94 op: String,
95 #[serde(default)]
96 payload: Value,
97 },
98 ExtensionCommands { commands: Vec<ExtensionCommandItem> },
100 AdvanceFrame { timestamp: u64 },
103}
104
105#[derive(Debug, Clone, Deserialize)]
107pub struct ExtensionCommandItem {
108 pub node_id: String,
109 pub op: String,
110 #[serde(default)]
111 pub payload: Value,
112}
113
114fn deserialize_binary_field<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
127where
128 D: Deserializer<'de>,
129{
130 use serde::de::Error;
131
132 let val: Option<Value> = Option::deserialize(deserializer)?;
133 match val {
134 None => Ok(None),
135 Some(Value::Null) => Ok(None),
136 Some(Value::String(s)) => {
138 use base64::Engine as _;
139 base64::engine::general_purpose::STANDARD
140 .decode(&s)
141 .map(Some)
142 .map_err(|e| D::Error::custom(format!("base64 decode: {e}")))
143 }
144 Some(Value::Array(arr)) => {
146 let bytes: Result<Vec<u8>, _> = arr
147 .into_iter()
148 .map(|v| {
149 v.as_u64()
150 .and_then(|n| u8::try_from(n).ok())
151 .ok_or_else(|| D::Error::custom("expected u8 in binary array"))
152 })
153 .collect();
154 bytes.map(Some)
155 }
156 Some(other) => Err(D::Error::custom(format!(
157 "expected string, array, or null for binary field, got {other}"
158 ))),
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use serde_json::json;
166
167 #[test]
172 fn deserialize_snapshot() {
173 let json =
174 r#"{"type":"snapshot","tree":{"id":"root","type":"column","props":{},"children":[]}}"#;
175 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
176 match msg {
177 IncomingMessage::Snapshot { tree } => {
178 assert_eq!(tree.id, "root");
179 assert_eq!(tree.type_name, "column");
180 }
181 _ => panic!("expected Snapshot"),
182 }
183 }
184
185 #[test]
186 fn deserialize_snapshot_nested_tree() {
187 let msg: IncomingMessage = serde_json::from_value(json!({
188 "type": "snapshot",
189 "tree": {
190 "id": "root",
191 "type": "column",
192 "props": { "spacing": 10 },
193 "children": [{
194 "id": "c1",
195 "type": "text",
196 "props": { "content": "hello" },
197 "children": []
198 }]
199 }
200 }))
201 .unwrap();
202 match msg {
203 IncomingMessage::Snapshot { tree } => {
204 assert_eq!(tree.children.len(), 1);
205 assert_eq!(tree.children[0].id, "c1");
206 assert_eq!(tree.children[0].type_name, "text");
207 assert_eq!(tree.props["spacing"], 10);
208 }
209 _ => panic!("expected Snapshot"),
210 }
211 }
212
213 #[test]
214 fn deserialize_patch_replace_node() {
215 let msg: IncomingMessage = serde_json::from_value(json!({
216 "type": "patch",
217 "ops": [{
218 "op": "replace_node",
219 "path": [0],
220 "node": {
221 "id": "x",
222 "type": "text",
223 "props": {},
224 "children": []
225 }
226 }]
227 }))
228 .unwrap();
229 match msg {
230 IncomingMessage::Patch { ops } => {
231 assert_eq!(ops.len(), 1);
232 assert_eq!(ops[0].op, "replace_node");
233 assert_eq!(ops[0].path, vec![0]);
234 assert!(ops[0].rest.get("node").is_some());
235 }
236 _ => panic!("expected Patch"),
237 }
238 }
239
240 #[test]
241 fn deserialize_patch_multiple_ops() {
242 let msg: IncomingMessage = serde_json::from_value(json!({
243 "type": "patch",
244 "ops": [
245 { "op": "update_props", "path": [0], "props": { "color": "red" } },
246 { "op": "remove_child", "path": [], "index": 2 }
247 ]
248 }))
249 .unwrap();
250 match msg {
251 IncomingMessage::Patch { ops } => {
252 assert_eq!(ops.len(), 2);
253 assert_eq!(ops[0].op, "update_props");
254 assert_eq!(ops[1].op, "remove_child");
255 }
256 _ => panic!("expected Patch"),
257 }
258 }
259
260 #[test]
261 fn deserialize_effect() {
262 let json = r#"{"type":"effect","id":"e1","kind":"clipboard_read","payload":{}}"#;
263 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
264 match msg {
265 IncomingMessage::Effect { id, kind, payload } => {
266 assert_eq!(id, "e1");
267 assert_eq!(kind, "clipboard_read");
268 assert!(payload.is_object());
269 }
270 _ => panic!("expected Effect"),
271 }
272 }
273
274 #[test]
275 fn deserialize_effect_with_payload() {
276 let msg: IncomingMessage = serde_json::from_value(json!({
277 "type": "effect",
278 "id": "e2",
279 "kind": "clipboard_write",
280 "payload": { "text": "copied" }
281 }))
282 .unwrap();
283 match msg {
284 IncomingMessage::Effect { id, kind, payload } => {
285 assert_eq!(id, "e2");
286 assert_eq!(kind, "clipboard_write");
287 assert_eq!(payload["text"], "copied");
288 }
289 _ => panic!("expected Effect"),
290 }
291 }
292
293 #[test]
294 fn deserialize_widget_op() {
295 let json = r#"{"type":"widget_op","op":"focus","payload":{"target":"input1"}}"#;
296 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
297 match msg {
298 IncomingMessage::WidgetOp { op, payload } => {
299 assert_eq!(op, "focus");
300 assert_eq!(payload["target"], "input1");
301 }
302 _ => panic!("expected WidgetOp"),
303 }
304 }
305
306 #[test]
307 fn deserialize_widget_op_no_payload() {
308 let json = r#"{"type":"widget_op","op":"blur"}"#;
309 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
310 match msg {
311 IncomingMessage::WidgetOp { op, payload } => {
312 assert_eq!(op, "blur");
313 assert!(payload.is_null());
314 }
315 _ => panic!("expected WidgetOp"),
316 }
317 }
318
319 #[test]
320 fn deserialize_subscribe() {
321 let json = r#"{"type":"subscribe","kind":"on_key_press","tag":"keys"}"#;
322 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
323 match msg {
324 IncomingMessage::Subscribe { kind, tag } => {
325 assert_eq!(kind, "on_key_press");
326 assert_eq!(tag, "keys");
327 }
328 _ => panic!("expected Subscribe"),
329 }
330 }
331
332 #[test]
333 fn deserialize_unsubscribe() {
334 let json = r#"{"type":"unsubscribe","kind":"on_key_press"}"#;
335 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
336 match msg {
337 IncomingMessage::Unsubscribe { kind } => {
338 assert_eq!(kind, "on_key_press");
339 }
340 _ => panic!("expected Unsubscribe"),
341 }
342 }
343
344 #[test]
345 fn deserialize_settings() {
346 let json = r#"{"type":"settings","settings":{"default_text_size":18}}"#;
347 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
348 match msg {
349 IncomingMessage::Settings { settings } => {
350 assert_eq!(settings["default_text_size"], 18);
351 }
352 _ => panic!("expected Settings"),
353 }
354 }
355
356 #[test]
357 fn deserialize_window_op() {
358 let msg: IncomingMessage = serde_json::from_value(json!({
359 "type": "window_op",
360 "op": "resize",
361 "window_id": "main",
362 "settings": { "width": 800, "height": 600 }
363 }))
364 .unwrap();
365 match msg {
366 IncomingMessage::WindowOp {
367 op,
368 window_id,
369 settings,
370 } => {
371 assert_eq!(op, "resize");
372 assert_eq!(window_id, "main");
373 assert_eq!(settings["width"], 800);
374 assert_eq!(settings["height"], 600);
375 }
376 _ => panic!("expected WindowOp"),
377 }
378 }
379
380 #[test]
381 fn deserialize_window_op_no_settings() {
382 let json = r#"{"type":"window_op","op":"close","window_id":"popup"}"#;
383 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
384 match msg {
385 IncomingMessage::WindowOp {
386 op,
387 window_id,
388 settings,
389 } => {
390 assert_eq!(op, "close");
391 assert_eq!(window_id, "popup");
392 assert!(settings.is_null());
393 }
394 _ => panic!("expected WindowOp"),
395 }
396 }
397
398 #[test]
399 fn deserialize_malformed_json_missing_field() {
400 let json = r#"{"type":"snapshot"}"#;
401 let result = serde_json::from_str::<IncomingMessage>(json);
402 assert!(result.is_err());
403 }
404
405 #[test]
406 fn deserialize_unknown_type_tag() {
407 let json = r#"{"type":"bogus_message","data":42}"#;
408 let result = serde_json::from_str::<IncomingMessage>(json);
409 assert!(result.is_err());
410 }
411
412 #[test]
413 fn deserialize_invalid_json_syntax() {
414 let json = r#"{"type":"snapshot",,,}"#;
415 let result = serde_json::from_str::<IncomingMessage>(json);
416 assert!(result.is_err());
417 }
418
419 #[test]
424 fn extension_command_deserializes() {
425 let msg: IncomingMessage = serde_json::from_value(json!({
426 "type": "extension_command",
427 "node_id": "term-1",
428 "op": "write",
429 "payload": { "data": "hello" }
430 }))
431 .unwrap();
432 match msg {
433 IncomingMessage::ExtensionCommand {
434 node_id,
435 op,
436 payload,
437 } => {
438 assert_eq!(node_id, "term-1");
439 assert_eq!(op, "write");
440 assert_eq!(payload["data"], "hello");
441 }
442 _ => panic!("wrong variant"),
443 }
444 }
445
446 #[test]
447 fn extension_commands_deserializes() {
448 let msg: IncomingMessage = serde_json::from_value(json!({
449 "type": "extension_commands",
450 "commands": [
451 { "node_id": "term-1", "op": "write", "payload": { "data": "a" } },
452 { "node_id": "log-1", "op": "append", "payload": { "line": "x" } }
453 ]
454 }))
455 .unwrap();
456 match msg {
457 IncomingMessage::ExtensionCommands { commands } => {
458 assert_eq!(commands.len(), 2);
459 assert_eq!(commands[0].node_id, "term-1");
460 assert_eq!(commands[1].op, "append");
461 }
462 _ => panic!("wrong variant"),
463 }
464 }
465
466 #[test]
467 fn extension_command_with_default_payload() {
468 let json = r#"{"type":"extension_command","node_id":"ext-1","op":"reset"}"#;
469 let msg: IncomingMessage = serde_json::from_str(json).unwrap();
470 match msg {
471 IncomingMessage::ExtensionCommand { payload, .. } => {
472 assert!(payload.is_null());
473 }
474 _ => panic!("wrong variant"),
475 }
476 }
477}