Skip to main content

wavecraft_bridge/
handler.rs

1//! IPC request handler.
2
3use crate::error::BridgeError;
4use crate::host::ParameterHost;
5use serde::Serialize;
6use wavecraft_protocol::{
7    GetAllParametersResult, GetAudioStatusResult, GetMeterFrameResult, GetParameterParams,
8    GetParameterResult, IpcRequest, IpcResponse, METHOD_GET_ALL_PARAMETERS,
9    METHOD_GET_AUDIO_STATUS, METHOD_GET_METER_FRAME, METHOD_GET_PARAMETER, METHOD_REQUEST_RESIZE,
10    METHOD_SET_PARAMETER, RequestId, RequestResizeParams, RequestResizeResult, SetParameterParams,
11    SetParameterResult,
12};
13
14/// IPC message handler that dispatches requests to a ParameterHost
15pub struct IpcHandler<H: ParameterHost> {
16    host: H,
17}
18
19impl<H: ParameterHost> IpcHandler<H> {
20    /// Create a new IPC handler with the given parameter host
21    pub fn new(host: H) -> Self {
22        Self { host }
23    }
24
25    /// Handle an incoming IPC request and produce a response
26    ///
27    /// This is the main entry point for processing messages from the UI.
28    /// It dispatches to appropriate handlers based on the method name.
29    pub fn handle_request(&self, request: IpcRequest) -> IpcResponse {
30        let result = match request.method.as_str() {
31            METHOD_GET_PARAMETER => self.handle_get_parameter(&request),
32            METHOD_SET_PARAMETER => self.handle_set_parameter(&request),
33            METHOD_GET_ALL_PARAMETERS => self.handle_get_all_parameters(&request),
34            METHOD_GET_METER_FRAME => self.handle_get_meter_frame(&request),
35            METHOD_GET_AUDIO_STATUS => self.handle_get_audio_status(&request),
36            METHOD_REQUEST_RESIZE => self.handle_request_resize(&request),
37            "ping" => self.handle_ping(&request),
38            _ => Err(BridgeError::UnknownMethod(request.method.clone())),
39        };
40
41        match result {
42            Ok(response) => response,
43            Err(err) => IpcResponse::error(request.id, err.to_ipc_error()),
44        }
45    }
46
47    /// Handle a raw JSON string request
48    ///
49    /// Convenience method that parses JSON and dispatches to handle_request.
50    pub fn handle_json(&self, json: &str) -> String {
51        // Parse request
52        let request: IpcRequest = match serde_json::from_str(json) {
53            Ok(req) => req,
54            Err(_e) => {
55                // Can't extract ID from malformed request, use null
56                let response = IpcResponse::error(
57                    RequestId::Number(0),
58                    wavecraft_protocol::IpcError::parse_error(),
59                );
60                // IpcResponse serialization is infallible: all fields are simple types
61                // (RequestId, Option<Value>, Option<IpcError>) that serde_json always handles
62                return serde_json::to_string(&response)
63                    .expect("IpcResponse serialization is infallible");
64            }
65        };
66
67        // Handle request
68        let response = self.handle_request(request);
69
70        // Serialize response - infallible for well-typed IpcResponse
71        serde_json::to_string(&response).expect("IpcResponse serialization is infallible")
72    }
73
74    // ------------------------------------------------------------------------
75    // Method Handlers
76    // ------------------------------------------------------------------------
77
78    fn handle_get_parameter(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
79        // Parse params
80        let params: GetParameterParams = match &request.params {
81            Some(value) => serde_json::from_value(value.clone())?,
82            None => {
83                return Err(BridgeError::InvalidParams {
84                    method: METHOD_GET_PARAMETER.to_string(),
85                    reason: "Missing params".to_string(),
86                });
87            }
88        };
89
90        // Get parameter from host
91        let param_info = self
92            .host
93            .get_parameter(&params.id)
94            .ok_or_else(|| BridgeError::ParameterNotFound(params.id.clone()))?;
95
96        // Build result
97        let result = GetParameterResult {
98            id: param_info.id,
99            value: param_info.value,
100        };
101
102        Ok(IpcResponse::success(request.id.clone(), result))
103    }
104
105    fn handle_set_parameter(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
106        // Parse params
107        let params: SetParameterParams = match &request.params {
108            Some(value) => serde_json::from_value(value.clone())?,
109            None => {
110                return Err(BridgeError::InvalidParams {
111                    method: METHOD_SET_PARAMETER.to_string(),
112                    reason: "Missing params".to_string(),
113                });
114            }
115        };
116
117        // Set parameter
118        self.host.set_parameter(&params.id, params.value)?;
119
120        // Build result (empty success)
121        Ok(IpcResponse::success(
122            request.id.clone(),
123            SetParameterResult {},
124        ))
125    }
126
127    fn handle_get_all_parameters(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
128        let parameters = self.host.get_all_parameters();
129
130        let result = GetAllParametersResult { parameters };
131
132        Ok(IpcResponse::success(request.id.clone(), result))
133    }
134
135    fn handle_get_meter_frame(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
136        // Get meter frame from host
137        let frame = self.host.get_meter_frame();
138
139        let result = GetMeterFrameResult { frame };
140
141        Ok(IpcResponse::success(request.id.clone(), result))
142    }
143
144    fn handle_request_resize(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
145        // Parse params
146        let params: RequestResizeParams = match &request.params {
147            Some(value) => serde_json::from_value(value.clone())?,
148            None => {
149                return Err(BridgeError::InvalidParams {
150                    method: METHOD_REQUEST_RESIZE.to_string(),
151                    reason: "Missing params".to_string(),
152                });
153            }
154        };
155
156        // Request resize from host
157        let accepted = self.host.request_resize(params.width, params.height);
158
159        let result = RequestResizeResult { accepted };
160
161        Ok(IpcResponse::success(request.id.clone(), result))
162    }
163
164    fn handle_get_audio_status(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
165        let result = GetAudioStatusResult {
166            status: self.host.get_audio_status(),
167        };
168
169        Ok(IpcResponse::success(request.id.clone(), result))
170    }
171
172    fn handle_ping(&self, request: &IpcRequest) -> Result<IpcResponse, BridgeError> {
173        // Simple ping/pong for testing connectivity
174        #[derive(Serialize)]
175        struct PingResult {
176            pong: bool,
177        }
178
179        Ok(IpcResponse::success(
180            request.id.clone(),
181            PingResult { pong: true },
182        ))
183    }
184}
185
186// ============================================================================
187// Tests
188// ============================================================================
189
190#[cfg(test)]
191mod tests {
192    use super::*;
193    use wavecraft_protocol::{
194        AudioRuntimePhase, AudioRuntimeStatus, MeterFrame, ParameterInfo, ParameterType, RequestId,
195    };
196
197    // Mock ParameterHost for testing
198    struct MockHost {
199        params: Vec<ParameterInfo>,
200    }
201
202    impl MockHost {
203        fn new() -> Self {
204            Self {
205                params: vec![
206                    ParameterInfo {
207                        id: "gain".to_string(),
208                        name: "Gain".to_string(),
209                        param_type: ParameterType::Float,
210                        value: 0.5,
211                        default: 0.7,
212                        min: 0.0,
213                        max: 1.0,
214                        unit: Some("dB".to_string()),
215                        group: None,
216                    },
217                    ParameterInfo {
218                        id: "bypass".to_string(),
219                        name: "Bypass".to_string(),
220                        param_type: ParameterType::Bool,
221                        value: 0.0,
222                        default: 0.0,
223                        min: 0.0,
224                        max: 1.0,
225                        unit: None,
226                        group: None,
227                    },
228                ],
229            }
230        }
231    }
232
233    impl ParameterHost for MockHost {
234        fn get_parameter(&self, id: &str) -> Option<ParameterInfo> {
235            self.params.iter().find(|p| p.id == id).cloned()
236        }
237
238        fn set_parameter(&self, id: &str, value: f32) -> Result<(), BridgeError> {
239            let Some(param) = self.params.iter().find(|p| p.id == id) else {
240                return Err(BridgeError::ParameterNotFound(id.to_string()));
241            };
242
243            if !(param.min..=param.max).contains(&value) {
244                return Err(BridgeError::ParameterOutOfRange {
245                    id: id.to_string(),
246                    value,
247                });
248            }
249
250            // In real implementation, would update atomic value
251            Ok(())
252        }
253
254        fn get_all_parameters(&self) -> Vec<ParameterInfo> {
255            self.params.clone()
256        }
257
258        fn get_meter_frame(&self) -> Option<MeterFrame> {
259            // Mock returns None
260            None
261        }
262
263        fn request_resize(&self, _width: u32, _height: u32) -> bool {
264            // Mock always accepts resize requests
265            true
266        }
267
268        fn get_audio_status(&self) -> Option<AudioRuntimeStatus> {
269            Some(AudioRuntimeStatus {
270                phase: AudioRuntimePhase::RunningFullDuplex,
271                diagnostic: None,
272                sample_rate: Some(44100.0),
273                buffer_size: Some(512),
274                updated_at_ms: 123,
275            })
276        }
277    }
278
279    #[test]
280    fn test_get_parameter_success() {
281        let handler = IpcHandler::new(MockHost::new());
282
283        let request = IpcRequest::new(
284            RequestId::Number(1),
285            METHOD_GET_PARAMETER,
286            Some(serde_json::json!({"id": "gain"})),
287        );
288
289        let response = handler.handle_request(request);
290
291        assert!(response.result.is_some());
292        assert!(response.error.is_none());
293
294        let result: GetParameterResult = serde_json::from_value(response.result.unwrap()).unwrap();
295        assert_eq!(result.id, "gain");
296        assert_eq!(result.value, 0.5);
297    }
298
299    #[test]
300    fn test_get_parameter_not_found() {
301        let handler = IpcHandler::new(MockHost::new());
302
303        let request = IpcRequest::new(
304            RequestId::Number(2),
305            METHOD_GET_PARAMETER,
306            Some(serde_json::json!({"id": "unknown"})),
307        );
308
309        let response = handler.handle_request(request);
310
311        assert!(response.error.is_some());
312        assert!(response.result.is_none());
313
314        let error = response.error.unwrap();
315        assert_eq!(error.code, wavecraft_protocol::ERROR_PARAM_NOT_FOUND);
316    }
317
318    #[test]
319    fn test_set_parameter_success() {
320        let handler = IpcHandler::new(MockHost::new());
321
322        let request = IpcRequest::new(
323            RequestId::Number(3),
324            METHOD_SET_PARAMETER,
325            Some(serde_json::json!({"id": "gain", "value": 0.8})),
326        );
327
328        let response = handler.handle_request(request);
329
330        assert!(response.result.is_some());
331        assert!(response.error.is_none());
332    }
333
334    #[test]
335    fn test_set_parameter_out_of_range() {
336        let handler = IpcHandler::new(MockHost::new());
337
338        let request = IpcRequest::new(
339            RequestId::Number(4),
340            METHOD_SET_PARAMETER,
341            Some(serde_json::json!({"id": "gain", "value": 1.5})),
342        );
343
344        let response = handler.handle_request(request);
345
346        assert!(response.error.is_some());
347        assert!(response.result.is_none());
348
349        let error = response.error.unwrap();
350        assert_eq!(error.code, wavecraft_protocol::ERROR_PARAM_OUT_OF_RANGE);
351    }
352
353    #[test]
354    fn test_unknown_method() {
355        let handler = IpcHandler::new(MockHost::new());
356
357        let request = IpcRequest::new(RequestId::Number(5), "unknownMethod", None);
358
359        let response = handler.handle_request(request);
360
361        assert!(response.error.is_some());
362        let error = response.error.unwrap();
363        assert_eq!(error.code, wavecraft_protocol::ERROR_METHOD_NOT_FOUND);
364    }
365
366    #[test]
367    fn test_ping() {
368        let handler = IpcHandler::new(MockHost::new());
369
370        let request = IpcRequest::new(RequestId::String("ping-1".to_string()), "ping", None);
371
372        let response = handler.handle_request(request);
373
374        assert!(response.result.is_some());
375        assert!(response.error.is_none());
376    }
377
378    #[test]
379    fn test_get_all_parameters() {
380        let handler = IpcHandler::new(MockHost::new());
381
382        let request = IpcRequest::new(RequestId::Number(6), METHOD_GET_ALL_PARAMETERS, None);
383
384        let response = handler.handle_request(request);
385
386        assert!(response.result.is_some());
387
388        let result: GetAllParametersResult =
389            serde_json::from_value(response.result.unwrap()).unwrap();
390        assert_eq!(result.parameters.len(), 2);
391    }
392
393    #[test]
394    fn test_handle_json() {
395        let handler = IpcHandler::new(MockHost::new());
396
397        let json = r#"{"jsonrpc":"2.0","id":1,"method":"getParameter","params":{"id":"gain"}}"#;
398        let response_json = handler.handle_json(json);
399
400        assert!(response_json.contains("\"result\""));
401        assert!(!response_json.contains("\"error\""));
402    }
403
404    #[test]
405    fn test_handle_json_parse_error() {
406        let handler = IpcHandler::new(MockHost::new());
407
408        let json = r#"{"invalid json"#;
409        let response_json = handler.handle_json(json);
410
411        assert!(response_json.contains("\"error\""));
412    }
413
414    #[test]
415    fn test_get_audio_status() {
416        let handler = IpcHandler::new(MockHost::new());
417
418        let request = IpcRequest::new(RequestId::Number(7), METHOD_GET_AUDIO_STATUS, None);
419
420        let response = handler.handle_request(request);
421        assert!(response.result.is_some());
422
423        let result: GetAudioStatusResult =
424            serde_json::from_value(response.result.expect("audio status response should exist"))
425                .expect("audio status result should deserialize");
426        let status = result.status.expect("status should be present");
427        assert_eq!(status.phase, AudioRuntimePhase::RunningFullDuplex);
428    }
429}