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}