use std::collections::HashMap;
use crate::ir::{FieldIR, ParamDefault, ParamFieldIR, ParamKind, ParamType, ParamsIR};
pub fn validate(ir: &ParamsIR) -> syn::Result<()> {
check_unique_string_ids(ir)?;
check_no_hash_collisions(ir)?;
validate_param_attributes(ir)?;
Ok(())
}
fn check_unique_string_ids(ir: &ParamsIR) -> syn::Result<()> {
let mut seen: HashMap<&str, &syn::Ident> = HashMap::new();
for field in &ir.fields {
if let FieldIR::Param(param) = field {
if let Some(first_field) = seen.get(param.string_id.as_str()) {
return Err(syn::Error::new(
param.span,
format!(
"Duplicate parameter id \"{}\": already used by field `{}`",
param.string_id, first_field
),
));
}
seen.insert(¶m.string_id, ¶m.field_name);
}
}
Ok(())
}
fn check_no_hash_collisions(ir: &ParamsIR) -> syn::Result<()> {
let mut seen: HashMap<u32, &str> = HashMap::new();
for field in &ir.fields {
if let FieldIR::Param(param) = field {
if let Some(first_id) = seen.get(¶m.hash_id) {
return Err(syn::Error::new(
param.span,
format!(
"Parameter ID hash collision: \"{}\" and \"{}\" both hash to 0x{:08x}. \
Rename one of these parameters to avoid the collision.",
param.string_id, first_id, param.hash_id
),
));
}
seen.insert(param.hash_id, ¶m.string_id);
}
}
Ok(())
}
fn validate_param_attributes(ir: &ParamsIR) -> syn::Result<()> {
for field in &ir.fields {
if let FieldIR::Param(param) = field {
validate_single_param(param)?;
}
}
Ok(())
}
fn validate_single_param(param: &ParamFieldIR) -> syn::Result<()> {
validate_range_ordering(param)?;
validate_default_in_range(param)?;
validate_smoothing_time(param)?;
validate_kind_type_consistency(param)?;
Ok(())
}
fn validate_range_ordering(param: &ParamFieldIR) -> syn::Result<()> {
if let Some(range) = ¶m.attrs.range {
if range.start >= range.end {
return Err(syn::Error::new(
range.span,
format!(
"invalid range: start ({}) must be less than end ({})",
format_number(range.start, param.param_type),
format_number(range.end, param.param_type),
),
));
}
}
Ok(())
}
fn validate_default_in_range(param: &ParamFieldIR) -> syn::Result<()> {
let (default_val, range) = match (¶m.attrs.default, ¶m.attrs.range) {
(Some(d), Some(r)) => (d, r),
_ => return Ok(()), };
let default_f64 = match default_val {
ParamDefault::Float(v) => *v,
ParamDefault::Int(v) => *v as f64,
ParamDefault::Bool(_) => return Ok(()), };
if default_f64 < range.start || default_f64 > range.end {
return Err(syn::Error::new(
param.span,
format!(
"default value {} is outside range {}..={}",
format_number(default_f64, param.param_type),
format_number(range.start, param.param_type),
format_number(range.end, param.param_type),
),
));
}
Ok(())
}
fn format_number(value: f64, param_type: ParamType) -> String {
match param_type {
ParamType::Float => {
if value.fract() == 0.0 {
format!("{:.1}", value) } else {
format!("{}", value)
}
}
ParamType::Int => format!("{}", value as i64),
_ => format!("{}", value),
}
}
fn validate_smoothing_time(param: &ParamFieldIR) -> syn::Result<()> {
if let Some(smoothing) = ¶m.attrs.smoothing {
if smoothing.time_ms <= 0.0 {
return Err(syn::Error::new(
smoothing.span,
format!(
"smoothing time must be positive, got {} ms",
smoothing.time_ms
),
));
}
}
Ok(())
}
fn validate_kind_type_consistency(param: &ParamFieldIR) -> syn::Result<()> {
let kind = match param.attrs.kind {
Some(k) => k,
None => return Ok(()), };
match (param.param_type, kind) {
(ParamType::Float, ParamKind::Semitones) => {
return Err(syn::Error::new(
param.span,
"kind 'semitones' should be used with IntParam, not FloatParam",
));
}
(ParamType::Int, ParamKind::Db | ParamKind::Hz | ParamKind::Ms | ParamKind::Seconds | ParamKind::Percent | ParamKind::Pan | ParamKind::Ratio) => {
return Err(syn::Error::new(
param.span,
format!(
"kind '{:?}' should be used with FloatParam, not IntParam",
kind
),
));
}
(ParamType::Bool, _) if !param.attrs.bypass => {
return Err(syn::Error::new(
param.span,
"BoolParam should not have a 'kind' attribute",
));
}
(ParamType::Enum, _) => {
return Err(syn::Error::new(
param.span,
"EnumParam should not have a 'kind' attribute",
));
}
_ => {}
}
Ok(())
}