use crate::error::ValidationError;
use std::ops::RangeInclusive;
pub trait Validatable {
fn validate(&self) -> Result<(), ValidationError>;
}
#[derive(Debug, Clone, Copy)]
pub struct RangeRule<T: PartialOrd> {
pub min: T,
pub max: T,
pub field: &'static str,
}
impl<T: PartialOrd + std::fmt::Display + Clone> RangeRule<T> {
pub const fn new(min: T, max: T, field: &'static str) -> Self {
Self { min, max, field }
}
pub fn validate(&self, value: &T) -> Result<(), ValidationError> {
if value < &self.min || value > &self.max {
Err(ValidationError::out_of_range(
self.field,
self.min.clone(),
self.max.clone(),
value.clone(),
))
} else {
Ok(())
}
}
pub fn range(&self) -> RangeInclusive<T> {
self.min.clone()..=self.max.clone()
}
}
impl RangeRule<f32> {
pub fn validate_f32(&self, value: f32) -> Result<(), ValidationError> {
if value < self.min || value > self.max {
Err(ValidationError::out_of_range(
self.field, self.min, self.max, value,
))
} else {
Ok(())
}
}
}
impl RangeRule<usize> {
pub fn validate_usize(&self, value: usize) -> Result<(), ValidationError> {
if value < self.min || value > self.max {
Err(ValidationError::out_of_range(
self.field, self.min, self.max, value,
))
} else {
Ok(())
}
}
}
pub mod rules {
use super::RangeRule;
use crate::config_defaults::{agent, environment, population, time, trail};
pub const SENSOR_ANGLE: RangeRule<f32> = RangeRule::new(
agent::MIN_SENSOR_ANGLE,
agent::MAX_SENSOR_ANGLE,
"sensor_angle",
);
pub const SENSOR_DISTANCE: RangeRule<f32> = RangeRule::new(
agent::MIN_SENSOR_DISTANCE,
agent::MAX_SENSOR_DISTANCE,
"sensor_distance",
);
pub const ROTATION_ANGLE: RangeRule<f32> = RangeRule::new(
agent::MIN_ROTATION_ANGLE,
agent::MAX_ROTATION_ANGLE,
"rotation_angle",
);
pub const STEP_SIZE: RangeRule<f32> =
RangeRule::new(agent::MIN_STEP_SIZE, agent::MAX_STEP_SIZE, "step_size");
pub const DEPOSIT_AMOUNT: RangeRule<f32> = RangeRule::new(
agent::MIN_DEPOSIT_AMOUNT,
agent::MAX_DEPOSIT_AMOUNT,
"deposit_amount",
);
pub const DECAY_FACTOR: RangeRule<f32> = RangeRule::new(
trail::MIN_DECAY_FACTOR,
trail::MAX_DECAY_FACTOR,
"decay_factor",
);
pub const MAX_BRIGHTNESS: RangeRule<f32> = RangeRule::new(
trail::MIN_MAX_BRIGHTNESS,
trail::MAX_MAX_BRIGHTNESS,
"max_brightness",
);
pub const DIFFUSION_SIGMA: RangeRule<f32> = RangeRule::new(
trail::MIN_DIFFUSION_SIGMA,
trail::MAX_DIFFUSION_SIGMA,
"diffusion_sigma",
);
pub const DECAY_GAMMA: RangeRule<f32> = RangeRule::new(
trail::MIN_DECAY_GAMMA,
trail::MAX_DECAY_GAMMA,
"decay_gamma",
);
pub const AFTERGLOW: RangeRule<f32> =
RangeRule::new(trail::MIN_AFTERGLOW, trail::MAX_AFTERGLOW, "afterglow");
pub const AFTERGLOW_RATE: RangeRule<f32> = RangeRule::new(
trail::MIN_AFTERGLOW_RATE,
trail::MAX_AFTERGLOW_RATE,
"afterglow_rate",
);
pub const DIFFUSE_WEIGHT: RangeRule<f32> = RangeRule::new(
trail::MIN_DIFFUSE_WEIGHT,
trail::MAX_DIFFUSE_WEIGHT,
"diffuse_weight",
);
pub const DEPOSIT_SCALE: RangeRule<f32> = RangeRule::new(
trail::MIN_DEPOSIT_SCALE,
trail::MAX_DEPOSIT_SCALE,
"deposit_scale",
);
pub const DEPOSIT_GAMMA: RangeRule<f32> = RangeRule::new(
trail::MIN_DEPOSIT_GAMMA,
trail::MAX_DEPOSIT_GAMMA,
"deposit_gamma",
);
pub const DEPOSIT_CAP: RangeRule<f32> = RangeRule::new(
trail::MIN_DEPOSIT_CAP,
trail::MAX_DEPOSIT_CAP,
"deposit_cap",
);
pub const TIME_SCALE: RangeRule<f32> =
RangeRule::new(time::MIN_TIME_SCALE, time::MAX_TIME_SCALE, "time_scale");
pub const ATTRACTOR_STRENGTH: RangeRule<f32> = RangeRule::new(
environment::MIN_ATTRACTOR_STRENGTH,
environment::MAX_ATTRACTOR_STRENGTH,
"attractor_strength",
);
pub const TERRAIN_STRENGTH: RangeRule<f32> = RangeRule::new(
environment::MIN_TERRAIN_STRENGTH,
environment::MAX_TERRAIN_STRENGTH,
"terrain_strength",
);
pub const POPULATION: RangeRule<usize> = RangeRule::new(
population::MIN_POPULATION,
population::MAX_POPULATION,
"population",
);
}
pub fn validate_range<T: PartialOrd + std::fmt::Display>(
value: T,
min: T,
max: T,
name: &str,
) -> Result<(), ValidationError> {
if value < min || value > max {
Err(ValidationError::out_of_range(name, min, max, value))
} else {
Ok(())
}
}
pub fn validate_min<T: PartialOrd + std::fmt::Display>(
value: T,
min: T,
name: &str,
) -> Result<(), ValidationError> {
if value < min {
Err(ValidationError::below_minimum(name, min, value))
} else {
Ok(())
}
}
pub fn validate_max<T: PartialOrd + std::fmt::Display>(
value: T,
max: T,
name: &str,
) -> Result<(), ValidationError> {
if value > max {
Err(ValidationError::above_maximum(name, max, value))
} else {
Ok(())
}
}
pub fn validate_not_empty(value: &str, name: &str) -> Result<(), ValidationError> {
if value.trim().is_empty() {
Err(ValidationError::empty(name))
} else {
Ok(())
}
}
pub fn validate_vec_not_empty<T>(value: &[T], name: &str) -> Result<(), ValidationError> {
if value.is_empty() {
Err(ValidationError::custom(format!("{} cannot be empty", name)))
} else {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_validate_range_in_range() {
assert!(validate_range(5.0, 0.0, 10.0, "test").is_ok());
}
#[test]
fn test_validate_range_below_min() {
let result = validate_range(-1.0, 0.0, 10.0, "test_param");
assert!(result.is_err());
let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("test_param"));
assert!(err_msg.contains("0"));
assert!(err_msg.contains("10"));
}
#[test]
fn test_validate_range_above_max() {
let result = validate_range(15.0, 0.0, 10.0, "test_param");
assert!(result.is_err());
}
#[test]
fn test_range_rule_validate() {
let rule = RangeRule::new(0.0, 10.0, "test");
assert!(rule.validate_f32(5.0).is_ok());
assert!(rule.validate_f32(15.0).is_err());
}
#[test]
fn test_validation_rules() {
assert!(rules::SENSOR_ANGLE.validate_f32(22.5).is_ok());
assert!(rules::SENSOR_DISTANCE.validate_f32(9.0).is_ok());
assert!(rules::POPULATION.validate_usize(50_000).is_ok());
}
#[test]
fn test_validate_min() {
assert!(validate_min(5, 0, "test").is_ok());
assert!(validate_min(-1, 0, "test").is_err());
}
#[test]
fn test_validate_max() {
assert!(validate_max(5, 10, "test").is_ok());
assert!(validate_max(15, 10, "test").is_err());
}
#[test]
fn test_validate_not_empty() {
assert!(validate_not_empty("hello", "test").is_ok());
assert!(validate_not_empty("", "test").is_err());
assert!(validate_not_empty(" ", "test").is_err());
}
#[test]
fn test_validate_vec_not_empty() {
assert!(validate_vec_not_empty(&[1, 2, 3], "test").is_ok());
assert!(validate_vec_not_empty(&Vec::<i32>::new(), "test").is_err());
}
#[test]
fn diffusion_sigma_accepts_widened_range() {
assert!(rules::DIFFUSION_SIGMA.validate(&3.5).is_ok());
assert!(rules::DIFFUSION_SIGMA.validate(&4.0).is_ok());
assert!(rules::DIFFUSION_SIGMA.validate(&4.5).is_err());
}
#[test]
fn decay_gamma_rule_bounds() {
assert!(rules::DECAY_GAMMA.validate(&1.0).is_ok());
assert!(rules::DECAY_GAMMA.validate(&0.25).is_ok());
assert!(rules::DECAY_GAMMA.validate(&0.1).is_err());
}
#[test]
fn deposit_rules_accept_defaults_reject_out_of_range() {
use super::rules;
assert!(rules::DEPOSIT_SCALE.validate_f32(1.0).is_ok());
assert!(rules::DEPOSIT_SCALE.validate_f32(-0.1).is_err());
assert!(rules::DEPOSIT_SCALE.validate_f32(11.0).is_err());
assert!(rules::DEPOSIT_GAMMA.validate_f32(1.0).is_ok());
assert!(rules::DEPOSIT_GAMMA.validate_f32(0.05).is_err());
assert!(rules::DEPOSIT_CAP.validate_f32(0.0).is_ok());
assert!(rules::DEPOSIT_CAP.validate_f32(-1.0).is_err());
}
}