Skip to main content

aether_core/
schema.rs

1//! JSON Schema validation for presets and configurations.
2//!
3//! Provides schema definitions and validation for:
4//! - Preset files
5//! - Node configurations
6//! - Graph definitions
7//! - Parameter mappings
8//!
9//! # Example
10//!
11//! ```rust
12//! use aetherdsp_core::schema::{SchemaValidator, ValidationError};
13//!
14//! let validator = SchemaValidator::new();
15//!
16//! let preset_json = r#"{
17//!     "name": "My Preset",
18//!     "nodes": [],
19//!     "connections": []
20//! }"#;
21//!
22//! match validator.validate_preset(preset_json) {
23//!     Ok(_) => println!("Valid preset"),
24//!     Err(errors) => {
25//!         for error in errors {
26//!             println!("Validation error: {}", error);
27//!         }
28//!     }
29//! }
30//! ```
31
32use serde::{Deserialize, Serialize};
33use serde_json::Value;
34use std::collections::HashMap;
35
36/// Validation error.
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct ValidationError {
39    /// JSON path where error occurred (e.g., "/nodes/0/params/gain").
40    pub path: String,
41
42    /// Error message.
43    pub message: String,
44
45    /// Error kind.
46    pub kind: ValidationErrorKind,
47}
48
49impl ValidationError {
50    /// Creates a new validation error.
51    pub fn new(
52        path: impl Into<String>,
53        message: impl Into<String>,
54        kind: ValidationErrorKind,
55    ) -> Self {
56        Self {
57            path: path.into(),
58            message: message.into(),
59            kind,
60        }
61    }
62}
63
64impl std::fmt::Display for ValidationError {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        write!(f, "{}: {} ({})", self.path, self.message, self.kind)
67    }
68}
69
70impl std::error::Error for ValidationError {}
71
72/// Validation error kind.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ValidationErrorKind {
75    /// Missing required field.
76    MissingField,
77    /// Invalid type.
78    InvalidType,
79    /// Value out of range.
80    OutOfRange,
81    /// Invalid format.
82    InvalidFormat,
83    /// Duplicate value.
84    Duplicate,
85    /// Invalid reference.
86    InvalidReference,
87}
88
89impl std::fmt::Display for ValidationErrorKind {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match self {
92            Self::MissingField => write!(f, "missing field"),
93            Self::InvalidType => write!(f, "invalid type"),
94            Self::OutOfRange => write!(f, "out of range"),
95            Self::InvalidFormat => write!(f, "invalid format"),
96            Self::Duplicate => write!(f, "duplicate"),
97            Self::InvalidReference => write!(f, "invalid reference"),
98        }
99    }
100}
101
102/// Schema validator.
103pub struct SchemaValidator {
104    /// Registered node types and their parameter schemas.
105    node_schemas: HashMap<String, NodeSchema>,
106}
107
108impl SchemaValidator {
109    /// Creates a new schema validator.
110    pub fn new() -> Self {
111        Self {
112            node_schemas: HashMap::new(),
113        }
114    }
115
116    /// Registers a node schema.
117    pub fn register_node_schema(&mut self, node_type: impl Into<String>, schema: NodeSchema) {
118        self.node_schemas.insert(node_type.into(), schema);
119    }
120
121    /// Validates a preset JSON string.
122    ///
123    /// # Arguments
124    ///
125    /// * `json` - Preset JSON string
126    ///
127    /// # Returns
128    ///
129    /// `Ok(())` if valid, `Err(errors)` if invalid.
130    pub fn validate_preset(&self, json: &str) -> Result<(), Vec<ValidationError>> {
131        let value: Value = match serde_json::from_str(json) {
132            Ok(v) => v,
133            Err(e) => {
134                return Err(vec![ValidationError::new(
135                    "/",
136                    format!("Invalid JSON: {}", e),
137                    ValidationErrorKind::InvalidFormat,
138                )]);
139            }
140        };
141
142        let mut errors = Vec::new();
143
144        // Validate root object
145        if !value.is_object() {
146            errors.push(ValidationError::new(
147                "/",
148                "Root must be an object",
149                ValidationErrorKind::InvalidType,
150            ));
151            return Err(errors);
152        }
153
154        let obj = value.as_object().unwrap();
155
156        // Validate required fields
157        if !obj.contains_key("name") {
158            errors.push(ValidationError::new(
159                "/name",
160                "Missing required field 'name'",
161                ValidationErrorKind::MissingField,
162            ));
163        }
164
165        if !obj.contains_key("nodes") {
166            errors.push(ValidationError::new(
167                "/nodes",
168                "Missing required field 'nodes'",
169                ValidationErrorKind::MissingField,
170            ));
171        }
172
173        if !obj.contains_key("connections") {
174            errors.push(ValidationError::new(
175                "/connections",
176                "Missing required field 'connections'",
177                ValidationErrorKind::MissingField,
178            ));
179        }
180
181        // Validate nodes array
182        if let Some(nodes) = obj.get("nodes") {
183            if !nodes.is_array() {
184                errors.push(ValidationError::new(
185                    "/nodes",
186                    "Field 'nodes' must be an array",
187                    ValidationErrorKind::InvalidType,
188                ));
189            } else {
190                let nodes_array = nodes.as_array().unwrap();
191                let mut node_ids = std::collections::HashSet::new();
192
193                for (i, node) in nodes_array.iter().enumerate() {
194                    let path = format!("/nodes/{}", i);
195
196                    if !node.is_object() {
197                        errors.push(ValidationError::new(
198                            &path,
199                            "Node must be an object",
200                            ValidationErrorKind::InvalidType,
201                        ));
202                        continue;
203                    }
204
205                    let node_obj = node.as_object().unwrap();
206
207                    // Validate node ID
208                    if let Some(id) = node_obj.get("id") {
209                        if let Some(id_num) = id.as_u64() {
210                            if !node_ids.insert(id_num) {
211                                errors.push(ValidationError::new(
212                                    format!("{}/id", path),
213                                    format!("Duplicate node ID: {}", id_num),
214                                    ValidationErrorKind::Duplicate,
215                                ));
216                            }
217                        } else {
218                            errors.push(ValidationError::new(
219                                format!("{}/id", path),
220                                "Node ID must be a number",
221                                ValidationErrorKind::InvalidType,
222                            ));
223                        }
224                    } else {
225                        errors.push(ValidationError::new(
226                            format!("{}/id", path),
227                            "Missing required field 'id'",
228                            ValidationErrorKind::MissingField,
229                        ));
230                    }
231
232                    // Validate node type
233                    if !node_obj.contains_key("node_type") {
234                        errors.push(ValidationError::new(
235                            format!("{}/node_type", path),
236                            "Missing required field 'node_type'",
237                            ValidationErrorKind::MissingField,
238                        ));
239                    }
240                }
241            }
242        }
243
244        // Validate connections array
245        if let Some(connections) = obj.get("connections") {
246            if !connections.is_array() {
247                errors.push(ValidationError::new(
248                    "/connections",
249                    "Field 'connections' must be an array",
250                    ValidationErrorKind::InvalidType,
251                ));
252            } else {
253                let connections_array = connections.as_array().unwrap();
254
255                for (i, conn) in connections_array.iter().enumerate() {
256                    let path = format!("/connections/{}", i);
257
258                    if !conn.is_object() {
259                        errors.push(ValidationError::new(
260                            &path,
261                            "Connection must be an object",
262                            ValidationErrorKind::InvalidType,
263                        ));
264                        continue;
265                    }
266
267                    let conn_obj = conn.as_object().unwrap();
268
269                    // Validate required fields
270                    for field in &["from_node", "from_output", "to_node", "to_input"] {
271                        if !conn_obj.contains_key(*field) {
272                            errors.push(ValidationError::new(
273                                format!("{}/{}", path, field),
274                                format!("Missing required field '{}'", field),
275                                ValidationErrorKind::MissingField,
276                            ));
277                        }
278                    }
279                }
280            }
281        }
282
283        if errors.is_empty() {
284            Ok(())
285        } else {
286            Err(errors)
287        }
288    }
289
290    /// Validates a node configuration.
291    pub fn validate_node_config(
292        &self,
293        node_type: &str,
294        config: &Value,
295    ) -> Result<(), Vec<ValidationError>> {
296        let mut errors = Vec::new();
297
298        if let Some(schema) = self.node_schemas.get(node_type) {
299            // Validate parameters
300            if let Some(params) = config.get("params") {
301                if let Some(params_obj) = params.as_object() {
302                    for (param_name, param_value) in params_obj {
303                        if let Some(param_schema) = schema.params.get(param_name) {
304                            // Validate parameter value
305                            if let Some(value) = param_value.as_f64() {
306                                let value_f32 = value as f32;
307                                if value_f32 < param_schema.min || value_f32 > param_schema.max {
308                                    errors.push(ValidationError::new(
309                                        format!("/params/{}", param_name),
310                                        format!(
311                                            "Value {} out of range [{}, {}]",
312                                            value_f32, param_schema.min, param_schema.max
313                                        ),
314                                        ValidationErrorKind::OutOfRange,
315                                    ));
316                                }
317                            } else {
318                                errors.push(ValidationError::new(
319                                    format!("/params/{}", param_name),
320                                    "Parameter value must be a number",
321                                    ValidationErrorKind::InvalidType,
322                                ));
323                            }
324                        }
325                    }
326                }
327            }
328        }
329
330        if errors.is_empty() {
331            Ok(())
332        } else {
333            Err(errors)
334        }
335    }
336}
337
338impl Default for SchemaValidator {
339    fn default() -> Self {
340        Self::new()
341    }
342}
343
344/// Node schema definition.
345#[derive(Debug, Clone, Serialize, Deserialize)]
346pub struct NodeSchema {
347    /// Node type name.
348    pub node_type: String,
349
350    /// Parameter schemas.
351    pub params: HashMap<String, ParamSchema>,
352
353    /// Number of inputs.
354    pub num_inputs: usize,
355
356    /// Number of outputs.
357    pub num_outputs: usize,
358}
359
360impl NodeSchema {
361    /// Creates a new node schema.
362    pub fn new(node_type: impl Into<String>, num_inputs: usize, num_outputs: usize) -> Self {
363        Self {
364            node_type: node_type.into(),
365            params: HashMap::new(),
366            num_inputs,
367            num_outputs,
368        }
369    }
370
371    /// Adds a parameter schema.
372    pub fn add_param(&mut self, name: impl Into<String>, schema: ParamSchema) {
373        self.params.insert(name.into(), schema);
374    }
375}
376
377/// Parameter schema definition.
378#[derive(Debug, Clone, Serialize, Deserialize)]
379pub struct ParamSchema {
380    /// Parameter name.
381    pub name: String,
382
383    /// Minimum value.
384    pub min: f32,
385
386    /// Maximum value.
387    pub max: f32,
388
389    /// Default value.
390    pub default: f32,
391
392    /// Unit (e.g., "Hz", "dB").
393    pub unit: Option<String>,
394}
395
396impl ParamSchema {
397    /// Creates a new parameter schema.
398    pub fn new(name: impl Into<String>, min: f32, max: f32, default: f32) -> Self {
399        Self {
400            name: name.into(),
401            min,
402            max,
403            default,
404            unit: None,
405        }
406    }
407
408    /// Sets the unit.
409    pub fn with_unit(mut self, unit: impl Into<String>) -> Self {
410        self.unit = Some(unit.into());
411        self
412    }
413}
414
415#[cfg(test)]
416mod tests {
417    use super::*;
418
419    #[test]
420    fn test_validate_preset_valid() {
421        let validator = SchemaValidator::new();
422
423        let json = r#"{
424            "name": "Test Preset",
425            "nodes": [
426                {
427                    "id": 0,
428                    "node_type": "Oscillator",
429                    "params": {}
430                }
431            ],
432            "connections": []
433        }"#;
434
435        let result = validator.validate_preset(json);
436        assert!(result.is_ok());
437    }
438
439    #[test]
440    fn test_validate_preset_missing_name() {
441        let validator = SchemaValidator::new();
442
443        let json = r#"{
444            "nodes": [],
445            "connections": []
446        }"#;
447
448        let result = validator.validate_preset(json);
449        assert!(result.is_err());
450
451        let errors = result.unwrap_err();
452        assert_eq!(errors.len(), 1);
453        assert_eq!(errors[0].kind, ValidationErrorKind::MissingField);
454        assert!(errors[0].path.contains("name"));
455    }
456
457    #[test]
458    fn test_validate_preset_duplicate_node_id() {
459        let validator = SchemaValidator::new();
460
461        let json = r#"{
462            "name": "Test",
463            "nodes": [
464                {"id": 0, "node_type": "Oscillator"},
465                {"id": 0, "node_type": "Filter"}
466            ],
467            "connections": []
468        }"#;
469
470        let result = validator.validate_preset(json);
471        assert!(result.is_err());
472
473        let errors = result.unwrap_err();
474        assert!(errors
475            .iter()
476            .any(|e| e.kind == ValidationErrorKind::Duplicate));
477    }
478
479    #[test]
480    fn test_validate_preset_invalid_json() {
481        let validator = SchemaValidator::new();
482
483        let json = "{ invalid json }";
484
485        let result = validator.validate_preset(json);
486        assert!(result.is_err());
487
488        let errors = result.unwrap_err();
489        assert_eq!(errors.len(), 1);
490        assert_eq!(errors[0].kind, ValidationErrorKind::InvalidFormat);
491    }
492
493    #[test]
494    fn test_validate_node_config_param_out_of_range() {
495        let mut validator = SchemaValidator::new();
496
497        let mut schema = NodeSchema::new("Oscillator", 0, 1);
498        schema.add_param(
499            "frequency",
500            ParamSchema::new("frequency", 20.0, 20000.0, 440.0),
501        );
502        validator.register_node_schema("Oscillator", schema);
503
504        let config = serde_json::json!({
505            "params": {
506                "frequency": 30000.0
507            }
508        });
509
510        let result = validator.validate_node_config("Oscillator", &config);
511        assert!(result.is_err());
512
513        let errors = result.unwrap_err();
514        assert_eq!(errors.len(), 1);
515        assert_eq!(errors[0].kind, ValidationErrorKind::OutOfRange);
516    }
517
518    #[test]
519    fn test_node_schema_builder() {
520        let mut schema = NodeSchema::new("Filter", 1, 1);
521        schema.add_param(
522            "cutoff",
523            ParamSchema::new("cutoff", 20.0, 20000.0, 1000.0).with_unit("Hz"),
524        );
525        schema.add_param("resonance", ParamSchema::new("resonance", 0.0, 1.0, 0.5));
526
527        assert_eq!(schema.node_type, "Filter");
528        assert_eq!(schema.num_inputs, 1);
529        assert_eq!(schema.num_outputs, 1);
530        assert_eq!(schema.params.len(), 2);
531        assert!(schema.params.contains_key("cutoff"));
532        assert!(schema.params.contains_key("resonance"));
533    }
534
535    #[test]
536    fn test_validation_error_display() {
537        let error = ValidationError::new(
538            "/nodes/0/id",
539            "Duplicate node ID: 5",
540            ValidationErrorKind::Duplicate,
541        );
542
543        let display = format!("{}", error);
544        assert!(display.contains("/nodes/0/id"));
545        assert!(display.contains("Duplicate node ID: 5"));
546        assert!(display.contains("duplicate"));
547    }
548}