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/// Notification: Parameter changed (push from Rust to UI)
214pub const NOTIFICATION_PARAMETER_CHANGED: &str = "parameterChanged";
215
216// ============================================================================
217// Helper Constructors
218// ============================================================================
219
220impl IpcRequest {
221    /// Create a new request
222    pub fn new(
223        id: RequestId,
224        method: impl Into<String>,
225        params: Option<serde_json::Value>,
226    ) -> Self {
227        Self {
228            jsonrpc: "2.0".to_string(),
229            id,
230            method: method.into(),
231            params,
232        }
233    }
234}
235
236impl IpcResponse {
237    /// Create a success response
238    pub fn success(id: RequestId, result: impl Serialize) -> Self {
239        Self {
240            jsonrpc: "2.0".to_string(),
241            id,
242            result: Some(serde_json::to_value(result).unwrap()),
243            error: None,
244        }
245    }
246
247    /// Create an error response
248    pub fn error(id: RequestId, error: IpcError) -> Self {
249        Self {
250            jsonrpc: "2.0".to_string(),
251            id,
252            result: None,
253            error: Some(error),
254        }
255    }
256}
257
258impl IpcNotification {
259    /// Create a new notification
260    pub fn new(method: impl Into<String>, params: impl Serialize) -> Self {
261        Self {
262            jsonrpc: "2.0".to_string(),
263            method: method.into(),
264            params: Some(serde_json::to_value(params).unwrap()),
265        }
266    }
267}
268
269impl IpcError {
270    /// Create a new error
271    pub fn new(code: i32, message: impl Into<String>) -> Self {
272        Self {
273            code,
274            message: message.into(),
275            data: None,
276        }
277    }
278
279    /// Create an error with additional data
280    pub fn with_data(code: i32, message: impl Into<String>, data: impl Serialize) -> Self {
281        Self {
282            code,
283            message: message.into(),
284            data: Some(serde_json::to_value(data).unwrap()),
285        }
286    }
287
288    /// Parse error
289    pub fn parse_error() -> Self {
290        Self::new(ERROR_PARSE, "Parse error")
291    }
292
293    /// Invalid request error
294    pub fn invalid_request(reason: impl Into<String>) -> Self {
295        Self::new(
296            ERROR_INVALID_REQUEST,
297            format!("Invalid request: {}", reason.into()),
298        )
299    }
300
301    /// Method not found error
302    pub fn method_not_found(method: impl AsRef<str>) -> Self {
303        Self::new(
304            ERROR_METHOD_NOT_FOUND,
305            format!("Method not found: {}", method.as_ref()),
306        )
307    }
308
309    /// Invalid params error
310    pub fn invalid_params(reason: impl Into<String>) -> Self {
311        Self::new(
312            ERROR_INVALID_PARAMS,
313            format!("Invalid params: {}", reason.into()),
314        )
315    }
316
317    /// Internal error
318    pub fn internal_error(reason: impl Into<String>) -> Self {
319        Self::new(ERROR_INTERNAL, format!("Internal error: {}", reason.into()))
320    }
321
322    /// Parameter not found error
323    pub fn param_not_found(id: impl AsRef<str>) -> Self {
324        Self::new(
325            ERROR_PARAM_NOT_FOUND,
326            format!("Parameter not found: {}", id.as_ref()),
327        )
328    }
329
330    /// Parameter out of range error
331    pub fn param_out_of_range(id: impl AsRef<str>, value: f32) -> Self {
332        Self::new(
333            ERROR_PARAM_OUT_OF_RANGE,
334            format!("Parameter '{}' value {} out of range", id.as_ref(), value),
335        )
336    }
337}
338
339#[cfg(test)]
340mod tests {
341    use super::*;
342
343    #[test]
344    fn test_request_serialization() {
345        let req = IpcRequest::new(
346            RequestId::Number(1),
347            METHOD_GET_PARAMETER,
348            Some(serde_json::json!({"id": "gain"})),
349        );
350
351        let json = serde_json::to_string(&req).unwrap();
352        assert!(json.contains("\"jsonrpc\":\"2.0\""));
353        assert!(json.contains("\"method\":\"getParameter\""));
354    }
355
356    #[test]
357    fn test_response_serialization() {
358        let resp = IpcResponse::success(
359            RequestId::Number(1),
360            GetParameterResult {
361                id: "gain".to_string(),
362                value: 0.5,
363            },
364        );
365
366        let json = serde_json::to_string(&resp).unwrap();
367        assert!(json.contains("\"jsonrpc\":\"2.0\""));
368        assert!(json.contains("\"result\""));
369        assert!(!json.contains("\"error\""));
370    }
371
372    #[test]
373    fn test_error_response() {
374        let resp = IpcResponse::error(
375            RequestId::String("test".to_string()),
376            IpcError::method_not_found("unknownMethod"),
377        );
378
379        let json = serde_json::to_string(&resp).unwrap();
380        assert!(json.contains("\"error\""));
381        assert!(!json.contains("\"result\""));
382    }
383
384    #[test]
385    fn test_notification_serialization() {
386        let notif = IpcNotification::new(
387            NOTIFICATION_PARAMETER_CHANGED,
388            ParameterChangedNotification {
389                id: "gain".to_string(),
390                value: 0.8,
391            },
392        );
393
394        let json = serde_json::to_string(&notif).unwrap();
395        println!("Notification JSON: {}", json);
396        assert!(json.contains("\"jsonrpc\":\"2.0\""));
397        assert!(json.contains("\"method\":\"parameterChanged\""));
398        // The ParameterChangedNotification has an "id" field, which is OK
399        // We're checking that the notification itself doesn't have a request id
400    }
401}
402
403// ============================================================================
404// Metering Types
405// ============================================================================
406
407/// Meter frame data for UI visualization.
408///
409/// All values are in linear scale (not dB).
410#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
411pub struct MeterFrame {
412    /// Left channel peak (linear, 0.0 to 1.0+)
413    pub peak_l: f32,
414    /// Right channel peak (linear, 0.0 to 1.0+)
415    pub peak_r: f32,
416    /// Left channel RMS (linear, 0.0 to 1.0+)
417    pub rms_l: f32,
418    /// Right channel RMS (linear, 0.0 to 1.0+)
419    pub rms_r: f32,
420    /// Sample timestamp (monotonic)
421    pub timestamp: u64,
422}
423
424/// Result for getMeterFrame method
425#[derive(Debug, Clone, Serialize, Deserialize)]
426pub struct GetMeterFrameResult {
427    /// Latest meter frame, or null if no data available
428    pub frame: Option<MeterFrame>,
429}
430
431// ----------------------------------------------------------------------------
432// requestResize
433// ----------------------------------------------------------------------------
434
435/// Parameters for requestResize request
436#[derive(Debug, Clone, Serialize, Deserialize)]
437pub struct RequestResizeParams {
438    /// Desired width in logical pixels
439    pub width: u32,
440    /// Desired height in logical pixels
441    pub height: u32,
442}
443
444/// Result of requestResize request
445#[derive(Debug, Clone, Serialize, Deserialize)]
446pub struct RequestResizeResult {
447    /// Whether the host approved the resize
448    pub accepted: bool,
449}