bbx_plugin/
params.rs

1//! Parameter definition and code generation utilities.
2//!
3//! This module provides two ways to define plugin parameters:
4//!
5//! 1. **JSON-based**: Parse a `parameters.json` file using [`ParamsFile`]
6//! 2. **Programmatic**: Define parameters as const arrays using [`ParamDef`]
7//!
8//! Both approaches can generate Rust and C++ code for parameter indices.
9
10use serde::Deserialize;
11
12// ============================================================================
13// Programmatic Parameter Definition (const arrays)
14// ============================================================================
15
16/// Parameter type variants for programmatic declaration.
17#[derive(Debug, Clone, PartialEq)]
18pub enum ParamType {
19    /// Boolean parameter (on/off toggle).
20    Bool { default: bool },
21
22    /// Float parameter with range.
23    Float { min: f64, max: f64, default: f64 },
24
25    /// Choice parameter (dropdown/enum).
26    Choice {
27        choices: &'static [&'static str],
28        default_index: usize,
29    },
30}
31
32/// Parameter definition for programmatic declaration.
33///
34/// Use the const fn constructors to build parameter definitions:
35///
36/// ```ignore
37/// const PARAMETERS: &[ParamDef] = &[
38///     ParamDef::float("GAIN", "Gain", -60.0, 30.0, 0.0),
39///     ParamDef::bool("MONO", "Mono", false),
40///     ParamDef::choice("MODE", "Mode", &["A", "B", "C"], 0),
41/// ];
42/// ```
43#[derive(Debug, Clone)]
44pub struct ParamDef {
45    /// Parameter ID (used for code generation, e.g., "GAIN" → PARAM_GAIN).
46    pub id: &'static str,
47    /// Display name shown in the UI.
48    pub name: &'static str,
49    /// Parameter type and configuration.
50    pub param_type: ParamType,
51}
52
53impl ParamDef {
54    /// Create a boolean parameter.
55    pub const fn bool(id: &'static str, name: &'static str, default: bool) -> Self {
56        Self {
57            id,
58            name,
59            param_type: ParamType::Bool { default },
60        }
61    }
62
63    /// Create a float parameter with range.
64    pub const fn float(id: &'static str, name: &'static str, min: f64, max: f64, default: f64) -> Self {
65        Self {
66            id,
67            name,
68            param_type: ParamType::Float { min, max, default },
69        }
70    }
71
72    /// Create a choice parameter with options.
73    pub const fn choice(
74        id: &'static str,
75        name: &'static str,
76        choices: &'static [&'static str],
77        default_index: usize,
78    ) -> Self {
79        Self {
80            id,
81            name,
82            param_type: ParamType::Choice { choices, default_index },
83        }
84    }
85}
86
87/// Generate Rust parameter index constants from a const array of ParamDefs.
88///
89/// Output example:
90/// ```text
91/// pub const PARAM_GAIN: usize = 0;
92/// pub const PARAM_PAN: usize = 1;
93/// pub const PARAM_COUNT: usize = 2;
94/// ```
95pub fn generate_rust_indices_from_defs(params: &[ParamDef]) -> String {
96    let mut code = String::from("// Auto-generated parameter indices - DO NOT EDIT\n\n");
97
98    for (index, param) in params.iter().enumerate() {
99        code.push_str(&format!("pub const PARAM_{}: usize = {};\n", param.id, index));
100    }
101
102    code.push_str(&format!(
103        "\n#[allow(dead_code)]\npub const PARAM_COUNT: usize = {};\n",
104        params.len()
105    ));
106
107    code
108}
109
110/// Generate C header with parameter index constants from a const array of ParamDefs.
111///
112/// Output example:
113/// ```text
114/// #define PARAM_GAIN 0
115/// #define PARAM_PAN 1
116/// #define PARAM_COUNT 2
117/// static const char* PARAM_IDS[PARAM_COUNT] = { "GAIN", "PAN" };
118/// ```
119pub fn generate_c_header_from_defs(params: &[ParamDef]) -> String {
120    let mut content = String::new();
121    content.push_str("/* Auto-generated parameter indices - DO NOT EDIT */\n\n");
122    content.push_str("#ifndef BBX_PARAMS_H\n");
123    content.push_str("#define BBX_PARAMS_H\n\n");
124
125    for (index, param) in params.iter().enumerate() {
126        content.push_str(&format!("#define PARAM_{} {}\n", param.id, index));
127    }
128
129    content.push_str(&format!("\n#define PARAM_COUNT {}\n\n", params.len()));
130
131    // Generate PARAM_IDS array for dynamic iteration
132    if !params.is_empty() {
133        content.push_str("static const char* PARAM_IDS[PARAM_COUNT] = {\n");
134        for (i, param) in params.iter().enumerate() {
135            let comma = if i < params.len() - 1 { "," } else { "" };
136            content.push_str(&format!("    \"{}\"{}\n", param.id, comma));
137        }
138        content.push_str("};\n\n");
139    }
140
141    content.push_str("#endif /* BBX_PARAMS_H */\n");
142
143    content
144}
145
146// ============================================================================
147// JSON-based Parameter Definition (parameters.json)
148// ============================================================================
149
150/// JSON parameter definition (for parsing parameters.json).
151#[derive(Debug, Clone, Deserialize)]
152#[serde(rename_all = "camelCase")]
153pub struct JsonParamDef {
154    /// Parameter ID.
155    pub id: String,
156    /// Display name.
157    pub name: String,
158    /// Parameter type: "boolean", "float", or "choice".
159    #[serde(rename = "type")]
160    pub param_type: String,
161    /// Default value for boolean/float parameters.
162    #[serde(default)]
163    pub default_value: Option<serde_json::Value>,
164    /// Default index for choice parameters.
165    #[serde(default)]
166    pub default_value_index: Option<usize>,
167    /// Minimum value for float parameters.
168    #[serde(default)]
169    pub min: Option<f64>,
170    /// Maximum value for float parameters.
171    #[serde(default)]
172    pub max: Option<f64>,
173    /// Unit label for float parameters (e.g., "dB").
174    #[serde(default)]
175    pub unit: Option<String>,
176    /// Midpoint for skewed float parameters.
177    #[serde(default)]
178    pub midpoint: Option<f64>,
179    /// Step interval for float parameters.
180    #[serde(default)]
181    pub interval: Option<f64>,
182    /// Number of decimal places to display.
183    #[serde(default)]
184    pub fraction_digits: Option<u32>,
185    /// Available choices for choice parameters.
186    #[serde(default)]
187    pub choices: Option<Vec<String>>,
188}
189
190/// Container for a parameters.json file.
191#[derive(Debug, Clone, Deserialize)]
192pub struct ParamsFile {
193    /// List of parameter definitions.
194    pub parameters: Vec<JsonParamDef>,
195}
196
197impl ParamsFile {
198    /// Parse a parameters.json file from a JSON string.
199    pub fn from_json(json: &str) -> Result<Self, serde_json::Error> {
200        serde_json::from_str(json)
201    }
202
203    /// Generate Rust parameter index constants.
204    ///
205    /// Output example:
206    /// ```text
207    /// pub const PARAM_GAIN: usize = 0;
208    /// pub const PARAM_PAN: usize = 1;
209    /// pub const PARAM_COUNT: usize = 2;
210    /// ```
211    pub fn generate_rust_indices(&self) -> String {
212        let mut code = String::from("// Auto-generated from parameters.json - DO NOT EDIT\n\n");
213
214        for (index, param) in self.parameters.iter().enumerate() {
215            code.push_str(&format!("pub const PARAM_{}: usize = {};\n", param.id, index));
216        }
217
218        code.push_str(&format!(
219            "\n#[allow(dead_code)]\npub const PARAM_COUNT: usize = {};\n",
220            self.parameters.len()
221        ));
222
223        code
224    }
225
226    /// Generate C header with parameter index constants.
227    ///
228    /// Output example:
229    /// ```text
230    /// #define PARAM_GAIN 0
231    /// #define PARAM_PAN 1
232    /// #define PARAM_COUNT 2
233    /// static const char* PARAM_IDS[PARAM_COUNT] = { "GAIN", "PAN" };
234    /// ```
235    pub fn generate_c_header(&self) -> String {
236        let mut content = String::new();
237        content.push_str("/* Auto-generated from parameters.json - DO NOT EDIT */\n\n");
238        content.push_str("#ifndef BBX_PARAMS_H\n");
239        content.push_str("#define BBX_PARAMS_H\n\n");
240
241        for (index, param) in self.parameters.iter().enumerate() {
242            content.push_str(&format!("#define PARAM_{} {}\n", param.id, index));
243        }
244
245        content.push_str(&format!("\n#define PARAM_COUNT {}\n\n", self.parameters.len()));
246
247        // Generate PARAM_IDS array for dynamic iteration
248        if !self.parameters.is_empty() {
249            content.push_str("static const char* PARAM_IDS[PARAM_COUNT] = {\n");
250            for (i, param) in self.parameters.iter().enumerate() {
251                let comma = if i < self.parameters.len() - 1 { "," } else { "" };
252                content.push_str(&format!("    \"{}\"{}\n", param.id, comma));
253            }
254            content.push_str("};\n\n");
255        }
256
257        content.push_str("#endif /* BBX_PARAMS_H */\n");
258
259        content
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_param_def_constructors() {
269        let bool_param = ParamDef::bool("MONO", "Mono", false);
270        assert_eq!(bool_param.id, "MONO");
271        assert_eq!(bool_param.param_type, ParamType::Bool { default: false });
272
273        let float_param = ParamDef::float("GAIN", "Gain", -60.0, 30.0, 0.0);
274        assert_eq!(float_param.id, "GAIN");
275
276        let choice_param = ParamDef::choice("MODE", "Mode", &["A", "B"], 0);
277        assert_eq!(choice_param.id, "MODE");
278    }
279
280    #[test]
281    fn test_generate_rust_indices_from_defs() {
282        const PARAMS: &[ParamDef] = &[
283            ParamDef::float("GAIN", "Gain", -60.0, 30.0, 0.0),
284            ParamDef::bool("MONO", "Mono", false),
285        ];
286
287        let code = generate_rust_indices_from_defs(PARAMS);
288        assert!(code.contains("pub const PARAM_GAIN: usize = 0;"));
289        assert!(code.contains("pub const PARAM_MONO: usize = 1;"));
290        assert!(code.contains("#[allow(dead_code)]"));
291        assert!(code.contains("pub const PARAM_COUNT: usize = 2;"));
292    }
293
294    #[test]
295    fn test_generate_c_header_from_defs() {
296        const PARAMS: &[ParamDef] = &[
297            ParamDef::float("GAIN", "Gain", -60.0, 30.0, 0.0),
298            ParamDef::bool("MONO", "Mono", false),
299        ];
300
301        let header = generate_c_header_from_defs(PARAMS);
302        assert!(header.contains("#define PARAM_GAIN 0"));
303        assert!(header.contains("#define PARAM_MONO 1"));
304        assert!(header.contains("#define PARAM_COUNT 2"));
305        assert!(header.contains("static const char* PARAM_IDS[PARAM_COUNT]"));
306        assert!(header.contains("\"GAIN\""));
307        assert!(header.contains("\"MONO\""));
308    }
309
310    #[test]
311    fn test_params_file_from_json() {
312        let json = r#"{
313            "parameters": [
314                {"id": "GAIN", "name": "Gain", "type": "float", "min": -60.0, "max": 30.0, "defaultValue": 0.0},
315                {"id": "MONO", "name": "Mono", "type": "boolean", "defaultValue": false}
316            ]
317        }"#;
318
319        let params = ParamsFile::from_json(json).unwrap();
320        assert_eq!(params.parameters.len(), 2);
321        assert_eq!(params.parameters[0].id, "GAIN");
322        assert_eq!(params.parameters[1].id, "MONO");
323    }
324
325    #[test]
326    fn test_params_file_generate_indices() {
327        let json = r#"{"parameters": [{"id": "GAIN", "name": "Gain", "type": "float"}]}"#;
328        let params = ParamsFile::from_json(json).unwrap();
329
330        let rust_code = params.generate_rust_indices();
331        assert!(rust_code.contains("pub const PARAM_GAIN: usize = 0;"));
332
333        let c_header = params.generate_c_header();
334        assert!(c_header.contains("#define PARAM_GAIN 0"));
335        assert!(c_header.contains("static const char* PARAM_IDS[PARAM_COUNT]"));
336        assert!(c_header.contains("\"GAIN\""));
337    }
338}