Skip to main content

arora_websocket/
messages.rs

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