"""
Validation utilities for editor sections.
This module provides validation functions to ensure editor sections
are configured correctly, preventing bugs like using FILTER parameters
in AMP sections or vice versa.
Usage:
# Run validation directly
python jdxi_editor/ui/editors/validation.py
# Or run the unit tests
python -m unittest tests.test_editor_validation
# Or use in code
from jdxi_editor.ui.editors.validation import validate_digital_sections
errors = validate_digital_sections()
if errors:
# Handle validation errors
"""
from typing import Dict, List, Set
from jdxi_editor.ui.adsr.spec import ADSRSpec, ADSRStage
[docs]
def validate_adsr_spec(
section_name: str,
adsr_spec: Dict[ADSRStage, ADSRSpec],
expected_prefix: str,
) -> List[str]:
"""
Validate that spec_adsr uses parameters with the expected prefix.
Args:
section_name: Name of the section being validated (e.g., "DigitalAmpSection")
adsr_spec: The spec_adsr dictionary to validate
expected_prefix: Expected parameter prefix (e.g., "AMP_ENV", "FILTER_ENV")
Returns:
List of error messages (empty if validation passes)
"""
errors = []
if not adsr_spec:
return errors
# Map of ADSR stages to expected parameter name patterns
stage_to_param = {
ADSRStage.ATTACK: f"{expected_prefix}_ATTACK_TIME",
ADSRStage.DECAY: f"{expected_prefix}_DECAY_TIME",
ADSRStage.SUSTAIN: f"{expected_prefix}_SUSTAIN_LEVEL",
ADSRStage.RELEASE: f"{expected_prefix}_RELEASE_TIME",
}
# PEAK is optional and may not exist for all envelope types
if expected_prefix == "FILTER_ENV":
stage_to_param[ADSRStage.DEPTH] = f"{expected_prefix}_DEPTH"
for stage, spec in adsr_spec.items():
if not isinstance(spec, ADSRSpec):
errors.append(
f"{section_name}: spec_adsr[{stage}] is not an ADSRSpec instance"
)
continue
param = spec.param
if param is None:
errors.append(f"{section_name}: spec_adsr[{stage}].param is None")
continue
# Get parameter name
param_name = getattr(param, "name", None)
if param_name is None:
# Try to get it from the parameter object itself
param_name = str(param)
# Check if this stage should have a parameter
expected_param_pattern = stage_to_param.get(stage)
if expected_param_pattern is None:
# PEAK might not be required for all envelope types
if stage == ADSRStage.DEPTH and expected_prefix == "AMP_ENV":
# AMP doesn't have a PEAK parameter, so this is OK if missing
continue
else:
errors.append(
f"{section_name}: spec_adsr[{stage}] has unexpected stage "
f"(expected one of: {list(stage_to_param.keys())})"
)
continue
# Validate parameter name matches expected prefix
if not param_name.startswith(expected_prefix):
# Check if it's using the wrong prefix
wrong_prefixes = ["FILTER_ENV", "AMP_ENV", "OSC_PITCH_ENV"]
for wrong_prefix in wrong_prefixes:
if wrong_prefix != expected_prefix and param_name.startswith(
wrong_prefix
):
errors.append(
f"{section_name}: spec_adsr[{stage}] uses wrong parameter: "
f"{param_name} (expected {expected_param_pattern}, "
f"found {wrong_prefix} parameter)"
)
break
else:
errors.append(
f"{section_name}: spec_adsr[{stage}] parameter '{param_name}' "
f"doesn't match expected pattern '{expected_param_pattern}'"
)
return errors
[docs]
def validate_section_parameters(
section_name: str,
param_specs: List,
expected_prefixes: Set[str],
) -> List[str]:
"""
Validate that parameter specs (e.g. SLIDER_GROUPS['controls']) use parameters with expected prefixes.
Args:
section_name: Name of the section being validated
param_specs: List of SliderSpec/SwitchSpec/ComboBoxSpec objects
expected_prefixes: Set of allowed parameter name prefixes
Returns:
List of error messages (empty if validation passes)
"""
errors = []
for spec in param_specs:
param = getattr(spec, "param", None)
if param is None:
continue
param_name = getattr(param, "name", None)
if param_name is None:
param_name = str(param)
# Check if parameter name starts with any expected prefix
matches_prefix = any(
param_name.startswith(prefix) for prefix in expected_prefixes
)
if not matches_prefix:
# Check for common wrong prefixes
wrong_prefixes = {
"FILTER_ENV": ["AMP_ENV", "OSC_PITCH_ENV"],
"AMP_ENV": ["FILTER_ENV", "OSC_PITCH_ENV"],
"FILTER_": ["AMP_"],
"AMP_": ["FILTER_"],
}
found_wrong = False
for prefix, wrong_list in wrong_prefixes.items():
if param_name.startswith(prefix):
for wrong_prefix in wrong_list:
if wrong_prefix in expected_prefixes:
errors.append(
f"{section_name}: Parameter '{param_name}' uses wrong prefix "
f"'{prefix}' (should use '{wrong_prefix}')"
)
found_wrong = True
break
if found_wrong:
break
if not found_wrong:
errors.append(
f"{section_name}: Parameter '{param_name}' doesn't match "
f"expected prefixes: {expected_prefixes}"
)
return errors
[docs]
def validate_digital_sections() -> Dict[str, List[str]]:
"""
Validate all digital editor sections for correct parameter usage.
Returns:
Dictionary mapping section names to lists of error messages
"""
from jdxi_editor.ui.editors.digital.partial.amp.section import DigitalAmpSection
from jdxi_editor.ui.editors.digital.partial.filter.section import (
DigitalFilterSection,
)
all_errors = {}
# Validate DigitalAmpSection
amp_errors = []
if hasattr(DigitalAmpSection, "spec"):
if hasattr(DigitalAmpSection.spec, "adsr"):
amp_adsr_errors = validate_adsr_spec(
"DigitalAmpSection",
DigitalAmpSection.spec.adsr,
"AMP_ENV",
)
amp_errors.extend(amp_adsr_errors)
if hasattr(DigitalAmpSection, "SLIDER_GROUPS"):
amp_specs = DigitalAmpSection.spec.get("controls", [])
amp_param_errors = validate_section_parameters(
"DigitalAmpSection",
amp_specs,
{"AMP_", "LEVEL_", "CUTOFF_"},
)
amp_errors.extend(amp_param_errors)
if amp_errors:
all_errors["DigitalAmpSection"] = amp_errors
# Validate DigitalFilterSection
filter_errors = []
if hasattr(DigitalFilterSection, "spec_adsr"):
filter_adsr_errors = validate_adsr_spec(
"DigitalFilterSection",
DigitalFilterSection.spec_adsr,
"FILTER_ENV",
)
filter_errors.extend(filter_adsr_errors)
if hasattr(DigitalFilterSection, "SLIDER_GROUPS"):
filter_specs = DigitalFilterSection.spec.get("filter", [])
filter_param_errors = validate_section_parameters(
"DigitalFilterSection",
filter_specs,
{"FILTER_"},
)
filter_errors.extend(filter_param_errors)
if filter_errors:
all_errors["DigitalFilterSection"] = filter_errors
return all_errors
if __name__ == "__main__":
"""Run validation when executed directly."""
import sys
from pathlib import Path
# Add project root to path if running directly
[docs]
project_root = Path(__file__).parent.parent.parent.parent
if str(project_root) not in sys.path:
sys.path.insert(0, str(project_root))
errors = validate_digital_sections()
if errors:
print("❌ Validation failed!")
print("=" * 60)
for section_name, section_errors in errors.items():
print(f"\n{section_name}:")
for error in section_errors:
print(f" • {error}")
exit(1)
else:
print("✅ All sections validated successfully!")
exit(0)