Skip to main content

arora_websocket/
messages.rs

1//! WebSocket message types bridging the Arora API.
2//!
3//! Messages are serialized as JSON with a `type` field discriminator, and
4//! speak the Arora data-layer vocabulary: values are written to and read from
5//! **keys** (hierarchical paths into the store).
6
7use crate::key::KeyInfo;
8use crate::method::MethodInfo;
9use arora_types::value::Value;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13/// Messages received from WebSocket clients.
14///
15/// All incoming messages use a `type` field to discriminate the message kind.
16/// Example JSON: `{"type": "write_values", "values": {...}}`
17#[derive(Debug, Clone, Deserialize)]
18#[serde(tag = "type", rename_all = "snake_case")]
19pub enum Incoming {
20    /// Write values to keys.
21    ///
22    /// Example: `{"type": "write_values", "values": {"face/mouth": {"f64": 0.5}}}`
23    WriteValues {
24        /// Map of key paths to their new values
25        values: HashMap<String, Value>,
26    },
27
28    /// Read the current values of keys.
29    ///
30    /// Example: `{"type": "read_values", "keys": ["face/mouth", "face/eyes"]}`
31    ReadValues {
32        /// List of key paths to retrieve
33        keys: Vec<String>,
34    },
35
36    /// Request the list of available keys.
37    ///
38    /// Example: `{"type": "list_keys"}` or `{"type": "list_keys", "path": "face"}`
39    ListKeys {
40        /// Optional path prefix to filter keys
41        #[serde(default)]
42        path: Option<String>,
43    },
44
45    /// Request the list of available RPC methods.
46    ///
47    /// Example: `{"type": "list_methods"}` or `{"type": "list_methods", "path": "audio"}`
48    ListMethods {
49        /// Optional path prefix to filter methods
50        #[serde(default)]
51        path: Option<String>,
52    },
53
54    /// Invoke an RPC method.
55    ///
56    /// Example: `{"type": "invoke", "method": "reset", "request_id": "req-1"}`
57    Invoke {
58        /// Method path/name to invoke
59        method: String,
60
61        /// Arguments as key-value pairs
62        #[serde(default)]
63        args: HashMap<String, Value>,
64
65        /// Optional request ID for correlating responses
66        #[serde(default)]
67        request_id: Option<String>,
68    },
69}
70
71/// Messages sent to WebSocket clients.
72///
73/// All outgoing messages use a `type` field to discriminate the message kind.
74#[derive(Debug, Clone, Serialize)]
75#[serde(tag = "type", rename_all = "snake_case")]
76pub enum Outgoing {
77    /// Response to WriteValues message.
78    ///
79    /// Example: `{"type": "write_values_resp", "success": true}`
80    WriteValuesResp {
81        /// Whether the write was successful
82        success: bool,
83
84        /// Error message if success is false
85        #[serde(skip_serializing_if = "Option::is_none")]
86        message: Option<String>,
87    },
88
89    /// Response to ReadValues message.
90    ///
91    /// Example: `{"type": "read_values_resp", "values": {"face/mouth": {"f64": 0.5}}}`
92    ReadValuesResp {
93        /// Map of key paths to their current values
94        values: HashMap<String, Value>,
95    },
96
97    /// Response to ListKeys message.
98    ///
99    /// Example: `{"type": "list_keys_resp", "keys": [...]}`
100    ListKeysResp {
101        /// List of matching keys
102        keys: Vec<KeyInfo>,
103    },
104
105    /// Response to ListMethods message.
106    ///
107    /// Example: `{"type": "list_methods_resp", "methods": [...]}`
108    ListMethodsResp {
109        /// List of matching methods
110        methods: Vec<MethodInfo>,
111    },
112
113    /// Response to Invoke message.
114    ///
115    /// Example: `{"type": "invoke_resp", "success": true, "request_id": "req-1"}`
116    InvokeResp {
117        /// Whether the invocation was successful
118        success: bool,
119
120        /// Echo back request_id if provided in the request
121        #[serde(skip_serializing_if = "Option::is_none")]
122        request_id: Option<String>,
123
124        /// Return value from the method (if any)
125        #[serde(skip_serializing_if = "Option::is_none")]
126        value: Option<Value>,
127
128        /// Error message if success is false
129        #[serde(skip_serializing_if = "Option::is_none")]
130        message: Option<String>,
131    },
132
133    /// Generic error response for parse failures or unknown message types.
134    ///
135    /// Example: `{"type": "error", "message": "Invalid JSON"}`
136    Error {
137        /// Echo back request_id if available
138        #[serde(skip_serializing_if = "Option::is_none")]
139        request_id: Option<String>,
140
141        /// Error description
142        message: String,
143    },
144
145    /// Server-initiated push: values changed (e.g. the runtime wrote new
146    /// state). Sent unsolicited to subscribed clients — the live-edit feed.
147    ///
148    /// Example: `{"type": "values_changed", "values": {"face/mouth": {"f64": 0.5}}}`
149    ValuesChanged {
150        /// Map of key paths to their new values.
151        values: HashMap<String, Value>,
152    },
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    #[test]
160    fn test_incoming_write_values_deserialize() {
161        let json = r#"{"type": "write_values", "values": {"test/path": {"f64": 0.5}}}"#;
162        let msg: Incoming = serde_json::from_str(json).unwrap();
163        match msg {
164            Incoming::WriteValues { values } => {
165                assert!(values.contains_key("test/path"));
166            }
167            _ => panic!("Expected WriteValues message"),
168        }
169    }
170
171    #[test]
172    fn test_incoming_read_values_deserialize() {
173        let json = r#"{"type": "read_values", "keys": ["face/mouth", "face/eyes"]}"#;
174        let msg: Incoming = serde_json::from_str(json).unwrap();
175        match msg {
176            Incoming::ReadValues { keys } => {
177                assert_eq!(keys, vec!["face/mouth", "face/eyes"]);
178            }
179            _ => panic!("Expected ReadValues message"),
180        }
181    }
182
183    #[test]
184    fn test_incoming_list_keys_deserialize() {
185        let json = r#"{"type": "list_keys", "path": "face"}"#;
186        let msg: Incoming = serde_json::from_str(json).unwrap();
187        match msg {
188            Incoming::ListKeys { path } => {
189                assert_eq!(path, Some("face".to_string()));
190            }
191            _ => panic!("Expected ListKeys message"),
192        }
193    }
194
195    #[test]
196    fn test_incoming_invoke_deserialize() {
197        let json = r#"{"type": "invoke", "method": "reset", "request_id": "req-1"}"#;
198        let msg: Incoming = serde_json::from_str(json).unwrap();
199        match msg {
200            Incoming::Invoke {
201                method,
202                args,
203                request_id,
204            } => {
205                assert_eq!(method, "reset");
206                assert!(args.is_empty());
207                assert_eq!(request_id, Some("req-1".to_string()));
208            }
209            _ => panic!("Expected Invoke message"),
210        }
211    }
212
213    #[test]
214    fn test_outgoing_write_values_resp_serialize() {
215        let msg = Outgoing::WriteValuesResp {
216            success: true,
217            message: None,
218        };
219        let json = serde_json::to_string(&msg).unwrap();
220        assert!(json.contains(r#""type":"write_values_resp""#));
221        assert!(json.contains(r#""success":true"#));
222        assert!(!json.contains("message"));
223    }
224
225    #[test]
226    fn test_outgoing_invoke_resp_serialize() {
227        let msg = Outgoing::InvokeResp {
228            success: false,
229            request_id: Some("req-1".to_string()),
230            value: None,
231            message: Some("Method not found".to_string()),
232        };
233        let json = serde_json::to_string(&msg).unwrap();
234        assert!(json.contains(r#""type":"invoke_resp""#));
235        assert!(json.contains(r#""request_id":"req-1""#));
236        assert!(json.contains(r#""message":"Method not found""#));
237    }
238}