hypen_engine/serialize/
remote.rs

1use crate::reconcile::Patch;
2use serde::{Deserialize, Serialize};
3
4/// Messages sent over the wire for Remote UI
5#[derive(Debug, Clone, Serialize, Deserialize)]
6#[serde(tag = "type", rename_all = "camelCase")]
7pub enum RemoteMessage {
8    /// Initial tree sent when client connects
9    InitialTree(InitialTree),
10
11    /// Incremental patches
12    Patch(PatchStream),
13
14    /// Action dispatched from client
15    DispatchAction {
16        module: String,
17        action: String,
18        payload: Option<serde_json::Value>,
19    },
20
21    /// State update from host
22    StateUpdate {
23        module: String,
24        state: serde_json::Value,
25    },
26}
27
28/// Initial tree sent to clients
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct InitialTree {
31    /// Module name
32    pub module: String,
33
34    /// Initial state snapshot
35    pub state: serde_json::Value,
36
37    /// Initial patches to construct the tree
38    pub patches: Vec<Patch>,
39
40    /// Revision number (starts at 0)
41    pub revision: u64,
42
43    /// Optional integrity hash
44    #[serde(skip_serializing_if = "Option::is_none")]
45    pub hash: Option<String>,
46}
47
48impl InitialTree {
49    pub fn new(module: String, state: serde_json::Value, patches: Vec<Patch>) -> Self {
50        Self {
51            module,
52            state,
53            patches,
54            revision: 0,
55            hash: None,
56        }
57    }
58
59    pub fn with_hash(mut self, hash: String) -> Self {
60        self.hash = Some(hash);
61        self
62    }
63}
64
65/// Patch stream for incremental updates
66#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct PatchStream {
68    /// Module name
69    pub module: String,
70
71    /// Patches to apply
72    pub patches: Vec<Patch>,
73
74    /// Revision number (monotonically increasing)
75    pub revision: u64,
76
77    /// Optional integrity hash
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub hash: Option<String>,
80}
81
82impl PatchStream {
83    pub fn new(module: String, patches: Vec<Patch>, revision: u64) -> Self {
84        Self {
85            module,
86            patches,
87            revision,
88            hash: None,
89        }
90    }
91
92    pub fn with_hash(mut self, hash: String) -> Self {
93        self.hash = Some(hash);
94        self
95    }
96}
97
98/// Helper to serialize messages to JSON
99pub fn serialize_message(message: &RemoteMessage) -> Result<String, serde_json::Error> {
100    serde_json::to_string(message)
101}
102
103/// Helper to deserialize messages from JSON
104pub fn deserialize_message(json: &str) -> Result<RemoteMessage, serde_json::Error> {
105    serde_json::from_str(json)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_serialize_initial_tree() {
114        let initial = InitialTree::new(
115            "TestModule".to_string(),
116            serde_json::json!({"count": 0}),
117            vec![],
118        );
119
120        let message = RemoteMessage::InitialTree(initial);
121        let json = serialize_message(&message).unwrap();
122
123        assert!(json.contains("initialTree"));
124        assert!(json.contains("TestModule"));
125    }
126
127    #[test]
128    fn test_roundtrip() {
129        let message = RemoteMessage::DispatchAction {
130            module: "Test".to_string(),
131            action: "increment".to_string(),
132            payload: Some(serde_json::json!({"amount": 1})),
133        };
134
135        let json = serialize_message(&message).unwrap();
136        let deserialized = deserialize_message(&json).unwrap();
137
138        match deserialized {
139            RemoteMessage::DispatchAction { module, action, .. } => {
140                assert_eq!(module, "Test");
141                assert_eq!(action, "increment");
142            }
143            _ => panic!("Wrong message type"),
144        }
145    }
146}