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
19use serde::{Deserialize, Serialize};
20
21/// Request message sent from UI to Rust
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct IpcRequest {
24    /// JSON-RPC version (always "2.0")
25    pub jsonrpc: String,
26    /// Unique request identifier for matching responses
27    pub id: RequestId,
28    /// Method name to invoke
29    pub method: String,
30    /// Method parameters (method-specific)
31    #[serde(skip_serializing_if = "Option::is_none")]
32    pub params: Option<serde_json::Value>,
33}
34
35/// Response message sent from Rust to UI
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct IpcResponse {
38    /// JSON-RPC version (always "2.0")
39    pub jsonrpc: String,
40    /// Request ID this response corresponds to
41    pub id: RequestId,
42    /// Success result (mutually exclusive with error)
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub result: Option<serde_json::Value>,
45    /// Error result (mutually exclusive with result)
46    #[serde(skip_serializing_if = "Option::is_none")]
47    pub error: Option<IpcError>,
48}
49
50/// Notification message sent from Rust to UI (no response expected)
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct IpcNotification {
53    /// JSON-RPC version (always "2.0")
54    pub jsonrpc: String,
55    /// Event type
56    pub method: String,
57    /// Event data
58    #[serde(skip_serializing_if = "Option::is_none")]
59    pub params: Option<serde_json::Value>,
60}
61
62/// Request ID can be string or number
63#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
64#[serde(untagged)]
65pub enum RequestId {
66    String(String),
67    Number(i64),
68}
69
70/// Error returned in IpcResponse
71#[derive(Debug, Clone, Serialize, Deserialize)]
72pub struct IpcError {
73    /// Error code (see error code constants)
74    pub code: i32,
75    /// Human-readable error message
76    pub message: String,
77    /// Additional error data (optional)
78    #[serde(skip_serializing_if = "Option::is_none")]
79    pub data: Option<serde_json::Value>,
80}
81
82// ============================================================================
83// Error Codes (JSON-RPC 2.0 standard codes + custom extensions)
84// ============================================================================
85
86/// JSON-RPC parse error (invalid JSON)
87pub const ERROR_PARSE: i32 = -32700;
88/// JSON-RPC invalid request (malformed structure)
89pub const ERROR_INVALID_REQUEST: i32 = -32600;
90/// JSON-RPC method not found
91pub const ERROR_METHOD_NOT_FOUND: i32 = -32601;
92/// JSON-RPC invalid method parameters
93pub const ERROR_INVALID_PARAMS: i32 = -32602;
94/// JSON-RPC internal error
95pub const ERROR_INTERNAL: i32 = -32603;
96
97// Custom application error codes (start at -32000)
98/// Parameter not found
99pub const ERROR_PARAM_NOT_FOUND: i32 = -32000;
100/// Parameter value out of valid range
101pub const ERROR_PARAM_OUT_OF_RANGE: i32 = -32001;
102
103// ============================================================================
104// Method-Specific Types
105// ============================================================================
106
107// ----------------------------------------------------------------------------
108// getParameter
109// ----------------------------------------------------------------------------
110
111/// Parameters for getParameter request
112#[derive(Debug, Clone, Serialize, Deserialize)]
113pub struct GetParameterParams {
114    /// Parameter ID to retrieve
115    pub id: String,
116}
117
118/// Result of getParameter request
119#[derive(Debug, Clone, Serialize, Deserialize)]
120pub struct GetParameterResult {
121    /// Parameter ID
122    pub id: String,
123    /// Current normalized value [0.0, 1.0]
124    pub value: f32,
125}
126
127// ----------------------------------------------------------------------------
128// setParameter
129// ----------------------------------------------------------------------------
130
131/// Parameters for setParameter request
132#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct SetParameterParams {
134    /// Parameter ID to update
135    pub id: String,
136    /// New normalized value [0.0, 1.0]
137    pub value: f32,
138}
139
140/// Result of setParameter request (empty success)
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct SetParameterResult {}
143
144// ----------------------------------------------------------------------------
145// getAllParameters
146// ----------------------------------------------------------------------------
147
148/// Result of getAllParameters request
149#[derive(Debug, Clone, Serialize, Deserialize)]
150pub struct GetAllParametersResult {
151    /// List of all parameters with their metadata and current values
152    pub parameters: Vec<ParameterInfo>,
153}
154
155/// Information about a single parameter
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ParameterInfo {
158    /// Parameter ID (unique identifier)
159    pub id: String,
160    /// Human-readable name
161    pub name: String,
162    /// Parameter type (float, bool, enum, etc.)
163    #[serde(rename = "type")]
164    pub param_type: ParameterType,
165    /// Current normalized value [0.0, 1.0]
166    pub value: f32,
167    /// Default normalized value [0.0, 1.0]
168    pub default: f32,
169    /// Unit suffix for display (e.g., "dB", "%", "Hz")
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub unit: Option<String>,
172    /// Group name for UI organization (e.g., "Input", "Processing", "Output")
173    #[serde(skip_serializing_if = "Option::is_none")]
174    pub group: Option<String>,
175}
176
177/// Parameter type discriminator
178#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
179#[serde(rename_all = "lowercase")]
180pub enum ParameterType {
181    Float,
182    Bool,
183    Enum,
184}
185
186// ----------------------------------------------------------------------------
187// Notification: parameterChanged
188// ----------------------------------------------------------------------------
189
190/// Notification sent when a parameter changes (e.g., from host automation)
191#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct ParameterChangedNotification {
193    /// Parameter ID that changed
194    pub id: String,
195    /// New normalized value [0.0, 1.0]
196    pub value: f32,
197}
198
199// ============================================================================
200// Method Name Constants
201// ============================================================================
202
203/// Method: Get single parameter value
204pub const METHOD_GET_PARAMETER: &str = "getParameter";
205/// Method: Set single parameter value
206pub const METHOD_SET_PARAMETER: &str = "setParameter";
207/// Method: Get all parameters with metadata
208pub const METHOD_GET_ALL_PARAMETERS: &str = "getAllParameters";
209/// Method: Get current meter frame (peak/RMS levels)
210pub const METHOD_GET_METER_FRAME: &str = "getMeterFrame";
211/// Method: Request resize of editor window
212pub const METHOD_REQUEST_RESIZE: &str = "requestResize";
213/// Method: Register audio client with dev server
214pub const METHOD_REGISTER_AUDIO: &str = "registerAudio";
215/// Notification: Parameter changed (push from Rust to UI)
216pub const NOTIFICATION_PARAMETER_CHANGED: &str = "parameterChanged";
217/// Notification: Meter update from audio binary (push to browser)
218pub const NOTIFICATION_METER_UPDATE: &str = "meterUpdate";
219
220// ============================================================================
221// Helper Constructors
222// ============================================================================
223
224impl IpcRequest {
225    /// Create a new request
226    pub fn new(
227        id: RequestId,
228        method: impl Into<String>,
229        params: Option<serde_json::Value>,
230    ) -> Self {
231        Self {
232            jsonrpc: "2.0".to_string(),
233            id,
234            method: method.into(),
235            params,
236        }
237    }
238}
239
240impl IpcResponse {
241    /// Create a success response
242    pub fn success(id: RequestId, result: impl Serialize) -> Self {
243        Self {
244            jsonrpc: "2.0".to_string(),
245            id,
246            result: Some(serde_json::to_value(result).unwrap()),
247            error: None,
248        }
249    }
250
251    /// Create an error response
252    pub fn error(id: RequestId, error: IpcError) -> Self {
253        Self {
254            jsonrpc: "2.0".to_string(),
255            id,
256            result: None,
257            error: Some(error),
258        }
259    }
260}
261
262impl IpcNotification {
263    /// Create a new notification
264    pub fn new(method: impl Into<String>, params: impl Serialize) -> Self {
265        Self {
266            jsonrpc: "2.0".to_string(),
267            method: method.into(),
268            params: Some(serde_json::to_value(params).unwrap()),
269        }
270    }
271}
272
273impl IpcError {
274    /// Create a new error
275    pub fn new(code: i32, message: impl Into<String>) -> Self {
276        Self {
277            code,
278            message: message.into(),
279            data: None,
280        }
281    }
282
283    /// Create an error with additional data
284    pub fn with_data(code: i32, message: impl Into<String>, data: impl Serialize) -> Self {
285        Self {
286            code,
287            message: message.into(),
288            data: Some(serde_json::to_value(data).unwrap()),
289        }
290    }
291
292    /// Parse error
293    pub fn parse_error() -> Self {
294        Self::new(ERROR_PARSE, "Parse error")
295    }
296
297    /// Invalid request error
298    pub fn invalid_request(reason: impl Into<String>) -> Self {
299        Self::new(
300            ERROR_INVALID_REQUEST,
301            format!("Invalid request: {}", reason.into()),
302        )
303    }
304
305    /// Method not found error
306    pub fn method_not_found(method: impl AsRef<str>) -> Self {
307        Self::new(
308            ERROR_METHOD_NOT_FOUND,
309            format!("Method not found: {}", method.as_ref()),
310        )
311    }
312
313    /// Invalid params error
314    pub fn invalid_params(reason: impl Into<String>) -> Self {
315        Self::new(
316            ERROR_INVALID_PARAMS,
317            format!("Invalid params: {}", reason.into()),
318        )
319    }
320
321    /// Internal error
322    pub fn internal_error(reason: impl Into<String>) -> Self {
323        Self::new(ERROR_INTERNAL, format!("Internal error: {}", reason.into()))
324    }
325
326    /// Parameter not found error
327    pub fn param_not_found(id: impl AsRef<str>) -> Self {
328        Self::new(
329            ERROR_PARAM_NOT_FOUND,
330            format!("Parameter not found: {}", id.as_ref()),
331        )
332    }
333
334    /// Parameter out of range error
335    pub fn param_out_of_range(id: impl AsRef<str>, value: f32) -> Self {
336        Self::new(
337            ERROR_PARAM_OUT_OF_RANGE,
338            format!("Parameter '{}' value {} out of range", id.as_ref(), value),
339        )
340    }
341}
342
343#[cfg(test)]
344mod tests {
345    use super::*;
346
347    #[test]
348    fn test_request_serialization() {
349        let req = IpcRequest::new(
350            RequestId::Number(1),
351            METHOD_GET_PARAMETER,
352            Some(serde_json::json!({"id": "gain"})),
353        );
354
355        let json = serde_json::to_string(&req).unwrap();
356        assert!(json.contains("\"jsonrpc\":\"2.0\""));
357        assert!(json.contains("\"method\":\"getParameter\""));
358    }
359
360    #[test]
361    fn test_response_serialization() {
362        let resp = IpcResponse::success(
363            RequestId::Number(1),
364            GetParameterResult {
365                id: "gain".to_string(),
366                value: 0.5,
367            },
368        );
369
370        let json = serde_json::to_string(&resp).unwrap();
371        assert!(json.contains("\"jsonrpc\":\"2.0\""));
372        assert!(json.contains("\"result\""));
373        assert!(!json.contains("\"error\""));
374    }
375
376    #[test]
377    fn test_error_response() {
378        let resp = IpcResponse::error(
379            RequestId::String("test".to_string()),
380            IpcError::method_not_found("unknownMethod"),
381        );
382
383        let json = serde_json::to_string(&resp).unwrap();
384        assert!(json.contains("\"error\""));
385        assert!(!json.contains("\"result\""));
386    }
387
388    #[test]
389    fn test_notification_serialization() {
390        let notif = IpcNotification::new(
391            NOTIFICATION_PARAMETER_CHANGED,
392            ParameterChangedNotification {
393                id: "gain".to_string(),
394                value: 0.8,
395            },
396        );
397
398        let json = serde_json::to_string(&notif).unwrap();
399        println!("Notification JSON: {}", json);
400        assert!(json.contains("\"jsonrpc\":\"2.0\""));
401        assert!(json.contains("\"method\":\"parameterChanged\""));
402        // The ParameterChangedNotification has an "id" field, which is OK
403        // We're checking that the notification itself doesn't have a request id
404    }
405
406    #[test]
407    fn test_register_audio_serialization() {
408        let req = IpcRequest::new(
409            RequestId::String("audio-1".to_string()),
410            METHOD_REGISTER_AUDIO,
411            Some(serde_json::json!({
412                "client_id": "dev-audio",
413                "sample_rate": 44100.0,
414                "buffer_size": 512
415            })),
416        );
417
418        let json = serde_json::to_string(&req).unwrap();
419        assert!(json.contains("\"method\":\"registerAudio\""));
420        assert!(json.contains("\"sample_rate\":44100"));
421    }
422
423    #[test]
424    fn test_meter_update_notification() {
425        let notif = IpcNotification::new(
426            NOTIFICATION_METER_UPDATE,
427            MeterUpdateNotification {
428                timestamp_us: 1000,
429                left_peak: 0.5,
430                left_rms: 0.3,
431                right_peak: 0.6,
432                right_rms: 0.4,
433            },
434        );
435
436        let json = serde_json::to_string(&notif).unwrap();
437        assert!(json.contains("\"method\":\"meterUpdate\""));
438        assert!(json.contains("\"left_peak\":0.5"));
439    }
440}
441
442// ============================================================================
443// Metering Types
444// ============================================================================
445
446/// Meter frame data for UI visualization.
447///
448/// All values are in linear scale (not dB).
449#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
450pub struct MeterFrame {
451    /// Left channel peak (linear, 0.0 to 1.0+)
452    pub peak_l: f32,
453    /// Right channel peak (linear, 0.0 to 1.0+)
454    pub peak_r: f32,
455    /// Left channel RMS (linear, 0.0 to 1.0+)
456    pub rms_l: f32,
457    /// Right channel RMS (linear, 0.0 to 1.0+)
458    pub rms_r: f32,
459    /// Sample timestamp (monotonic)
460    pub timestamp: u64,
461}
462
463/// Result for getMeterFrame method
464#[derive(Debug, Clone, Serialize, Deserialize)]
465pub struct GetMeterFrameResult {
466    /// Latest meter frame, or null if no data available
467    pub frame: Option<MeterFrame>,
468}
469
470// ----------------------------------------------------------------------------
471// requestResize
472// ----------------------------------------------------------------------------
473
474/// Parameters for requestResize request
475#[derive(Debug, Clone, Serialize, Deserialize)]
476pub struct RequestResizeParams {
477    /// Desired width in logical pixels
478    pub width: u32,
479    /// Desired height in logical pixels
480    pub height: u32,
481}
482
483/// Result of requestResize request
484#[derive(Debug, Clone, Serialize, Deserialize)]
485pub struct RequestResizeResult {
486    /// Whether the host approved the resize
487    pub accepted: bool,
488}
489
490// ----------------------------------------------------------------------------
491// registerAudio
492// ----------------------------------------------------------------------------
493
494/// Parameters for registerAudio request (audio binary → dev server)
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct RegisterAudioParams {
497    /// Unique client identifier
498    pub client_id: String,
499    /// Audio sample rate (e.g., 44100.0)
500    pub sample_rate: f32,
501    /// Buffer size in samples
502    pub buffer_size: u32,
503}
504
505/// Result of registerAudio request
506#[derive(Debug, Clone, Serialize, Deserialize)]
507pub struct RegisterAudioResult {
508    /// Acknowledgment message
509    pub status: String,
510}
511
512// ----------------------------------------------------------------------------
513// Notification: meterUpdate
514// ----------------------------------------------------------------------------
515
516/// Notification sent from audio binary to browser via dev server
517#[derive(Debug, Clone, Serialize, Deserialize)]
518pub struct MeterUpdateNotification {
519    /// Timestamp in microseconds
520    pub timestamp_us: u64,
521    /// Left channel peak (linear scale)
522    pub left_peak: f32,
523    /// Left channel RMS (linear scale)
524    pub left_rms: f32,
525    /// Right channel peak (linear scale)
526    pub right_peak: f32,
527    /// Right channel RMS (linear scale)
528    pub right_rms: f32,
529}