Skip to main content

wavecraft_protocol/
ipc.rs

1//! IPC message contracts for WebView ↔ Rust communication
2//!
3//! This module defines JSON-RPC 2.0 style messages used for bidirectional
4//! communication between the React UI (running in WebView) and the Rust
5//! application logic.
6//!
7//! # Architecture
8//!
9//! - **Request/Response**: UI initiates, Rust responds (e.g., setParameter, getParameter)
10//! - **Notifications**: Rust pushes updates to UI (e.g., parameter changes from host)
11//!
12//! # JSON-RPC 2.0 Compatibility
13//!
14//! Messages follow JSON-RPC 2.0 conventions:
15//! - Requests have `id`, `method`, and `params`
16//! - Responses have `id` and either `result` or `error`
17//! - Notifications have `method` and `params` but no `id`
18
19#[path = "ipc/envelope.rs"]
20mod envelope;
21#[path = "ipc/errors.rs"]
22mod errors;
23#[path = "ipc/methods.rs"]
24mod methods;
25
26pub use envelope::{IpcNotification, IpcRequest, IpcResponse, RequestId};
27pub use errors::{
28    ERROR_INTERNAL, ERROR_INVALID_PARAMS, ERROR_INVALID_REQUEST, ERROR_METHOD_NOT_FOUND,
29    ERROR_PARAM_NOT_FOUND, ERROR_PARAM_OUT_OF_RANGE, ERROR_PARSE, IpcError,
30};
31pub use methods::{
32    AudioDiagnostic, AudioDiagnosticCode, AudioRuntimePhase, AudioRuntimeStatus,
33    GetAllParametersResult, GetAudioStatusResult, GetMeterFrameResult, GetOscilloscopeFrameResult,
34    GetParameterParams, GetParameterResult, METHOD_GET_ALL_PARAMETERS, METHOD_GET_AUDIO_STATUS,
35    METHOD_GET_METER_FRAME, METHOD_GET_OSCILLOSCOPE_FRAME, METHOD_GET_PARAMETER,
36    METHOD_REGISTER_AUDIO, METHOD_REQUEST_RESIZE, METHOD_SET_PARAMETER, MeterFrame,
37    MeterUpdateNotification, NOTIFICATION_AUDIO_STATUS_CHANGED, NOTIFICATION_METER_UPDATE,
38    NOTIFICATION_PARAMETER_CHANGED, OscilloscopeChannelView, OscilloscopeFrame,
39    OscilloscopeTriggerMode, ParameterChangedNotification, ParameterInfo, ParameterType,
40    ProcessorInfo, RegisterAudioParams, RegisterAudioResult, RequestResizeParams,
41    RequestResizeResult, SetParameterParams, SetParameterResult,
42};
43
44#[cfg(test)]
45mod tests {
46    use super::*;
47    use serde::Serialize;
48    use serde::ser::Error as _;
49
50    struct FailingSerialize;
51
52    impl Serialize for FailingSerialize {
53        fn serialize<S>(&self, _serializer: S) -> Result<S::Ok, S::Error>
54        where
55            S: serde::Serializer,
56        {
57            Err(S::Error::custom("intentional serialization failure"))
58        }
59    }
60
61    #[test]
62    fn test_request_serialization() {
63        let req = IpcRequest::new(
64            RequestId::Number(1),
65            METHOD_GET_PARAMETER,
66            Some(serde_json::json!({"id": "gain"})),
67        );
68
69        let json = serde_json::to_string(&req).unwrap();
70        assert!(json.contains("\"jsonrpc\":\"2.0\""));
71        assert!(json.contains("\"method\":\"getParameter\""));
72    }
73
74    #[test]
75    fn test_response_serialization() {
76        let resp = IpcResponse::success(
77            RequestId::Number(1),
78            GetParameterResult {
79                id: "gain".to_string(),
80                value: 0.5,
81            },
82        );
83
84        let json = serde_json::to_string(&resp).unwrap();
85        assert!(json.contains("\"jsonrpc\":\"2.0\""));
86        assert!(json.contains("\"result\""));
87        assert!(!json.contains("\"error\""));
88    }
89
90    #[test]
91    fn test_error_response() {
92        let resp = IpcResponse::error(
93            RequestId::String("test".to_string()),
94            IpcError::method_not_found("unknownMethod"),
95        );
96
97        let json = serde_json::to_string(&resp).unwrap();
98        assert!(json.contains("\"error\""));
99        assert!(!json.contains("\"result\""));
100    }
101
102    #[test]
103    fn test_notification_serialization() {
104        let notif = IpcNotification::new(
105            NOTIFICATION_PARAMETER_CHANGED,
106            ParameterChangedNotification {
107                id: "gain".to_string(),
108                value: 0.8,
109            },
110        );
111
112        let json = serde_json::to_string(&notif).unwrap();
113        println!("Notification JSON: {}", json);
114        assert!(json.contains("\"jsonrpc\":\"2.0\""));
115        assert!(json.contains("\"method\":\"parameterChanged\""));
116        // The ParameterChangedNotification has an "id" field, which is OK
117        // We're checking that the notification itself doesn't have a request id
118    }
119
120    #[test]
121    fn test_register_audio_serialization() {
122        let req = IpcRequest::new(
123            RequestId::String("audio-1".to_string()),
124            METHOD_REGISTER_AUDIO,
125            Some(serde_json::json!({
126                "client_id": "dev-audio",
127                "sample_rate": 44100.0,
128                "buffer_size": 512
129            })),
130        );
131
132        let json = serde_json::to_string(&req).unwrap();
133        assert!(json.contains("\"method\":\"registerAudio\""));
134        assert!(json.contains("\"sample_rate\":44100"));
135    }
136
137    #[test]
138    fn test_meter_update_notification() {
139        let notif = IpcNotification::new(
140            NOTIFICATION_METER_UPDATE,
141            MeterUpdateNotification {
142                timestamp_us: 1000,
143                left_peak: 0.5,
144                left_rms: 0.3,
145                right_peak: 0.6,
146                right_rms: 0.4,
147            },
148        );
149
150        let json = serde_json::to_string(&notif).unwrap();
151        assert!(json.contains("\"method\":\"meterUpdate\""));
152        assert!(json.contains("\"left_peak\":0.5"));
153    }
154
155    #[test]
156    fn test_audio_status_serialization() {
157        let result = GetAudioStatusResult {
158            status: Some(AudioRuntimeStatus {
159                phase: AudioRuntimePhase::RunningFullDuplex,
160                diagnostic: None,
161                sample_rate: Some(44100.0),
162                buffer_size: Some(512),
163                updated_at_ms: 123,
164            }),
165        };
166
167        let json = serde_json::to_string(&result).expect("status result should serialize");
168        assert!(json.contains("\"phase\":\"runningFullDuplex\""));
169        assert!(json.contains("\"sample_rate\":44100"));
170    }
171
172    #[test]
173    fn test_oscilloscope_frame_serialization() {
174        let result = GetOscilloscopeFrameResult {
175            frame: Some(OscilloscopeFrame {
176                points_l: vec![0.0; 1024],
177                points_r: vec![0.0; 1024],
178                sample_rate: 44100.0,
179                timestamp: 7,
180                no_signal: true,
181                trigger_mode: OscilloscopeTriggerMode::RisingZeroCrossing,
182            }),
183        };
184
185        let json = serde_json::to_string(&result).expect("oscilloscope result should serialize");
186        assert!(json.contains("\"sample_rate\":44100"));
187        assert!(json.contains("\"trigger_mode\":\"risingZeroCrossing\""));
188    }
189
190    #[test]
191    fn parameter_info_with_variants_serializes_correctly() {
192        let info = ParameterInfo {
193            id: "osc_waveform".to_string(),
194            name: "Waveform".to_string(),
195            param_type: ParameterType::Enum,
196            value: 0.0,
197            default: 0.0,
198            min: 0.0,
199            max: 3.0,
200            unit: None,
201            group: None,
202            variants: Some(vec![
203                "Sine".to_string(),
204                "Square".to_string(),
205                "Saw".to_string(),
206                "Triangle".to_string(),
207            ]),
208        };
209
210        let json = serde_json::to_string(&info).expect("parameter info should serialize");
211        assert!(json.contains("\"variants\""));
212
213        let deserialized: ParameterInfo =
214            serde_json::from_str(&json).expect("parameter info should deserialize");
215        assert_eq!(
216            deserialized.variants.expect("variants should exist").len(),
217            4
218        );
219    }
220
221    #[test]
222    fn parameter_info_without_variants_omits_field() {
223        let info = ParameterInfo {
224            id: "gain".to_string(),
225            name: "Gain".to_string(),
226            param_type: ParameterType::Float,
227            value: 0.5,
228            default: 0.5,
229            min: 0.0,
230            max: 1.0,
231            unit: Some("dB".to_string()),
232            group: None,
233            variants: None,
234        };
235
236        let json = serde_json::to_string(&info).expect("parameter info should serialize");
237        assert!(!json.contains("\"variants\""));
238    }
239
240    #[test]
241    fn try_success_returns_error_on_serialize_failure() {
242        let result = IpcResponse::try_success(RequestId::Number(1), FailingSerialize);
243        assert!(result.is_err());
244    }
245
246    #[test]
247    fn success_does_not_panic_on_serialize_failure() {
248        let response = IpcResponse::success(RequestId::Number(1), FailingSerialize);
249        assert!(response.result.is_none());
250        assert!(response.error.is_some());
251    }
252
253    #[test]
254    fn try_notification_returns_error_on_serialize_failure() {
255        let result = IpcNotification::try_new(NOTIFICATION_PARAMETER_CHANGED, FailingSerialize);
256        assert!(result.is_err());
257    }
258
259    #[test]
260    fn notification_new_does_not_panic_on_serialize_failure() {
261        let notification = IpcNotification::new(NOTIFICATION_PARAMETER_CHANGED, FailingSerialize);
262        assert!(notification.params.is_none());
263    }
264
265    #[test]
266    fn try_with_data_returns_error_on_serialize_failure() {
267        let result = IpcError::try_with_data(ERROR_INTERNAL, "test", FailingSerialize);
268        assert!(result.is_err());
269    }
270
271    #[test]
272    fn with_data_does_not_panic_on_serialize_failure() {
273        let error = IpcError::with_data(ERROR_INTERNAL, "test", FailingSerialize);
274        assert!(error.data.is_none());
275        assert_eq!(error.message, "test");
276    }
277}