use std::any::Any;
use std::fmt::Debug;
#[derive(Debug, Clone)]
pub enum ParamType {
Number { min: f64, max: f64 },
Boolean,
Enum { options: &'static [&'static str] },
Composite,
}
pub trait PatternParam: Debug + Any {
fn name(&self) -> &'static str;
fn description(&self) -> &'static str;
fn param_type(&self) -> ParamType;
fn default_value(&self) -> String;
fn validate(&self, value: &str) -> Result<(), String>;
fn parse(&self, value: &str) -> Result<Box<dyn PatternParam>, String>;
fn sub_params(&self) -> Vec<Box<dyn PatternParam>> {
Vec::new()
}
fn clone_param(&self) -> Box<dyn PatternParam>;
fn as_any(&self) -> &dyn Any;
}
#[macro_export]
macro_rules! define_param {
($(#[$meta:meta])* num $pattern:ident, $name:ident, $cli_name:expr, $desc:expr, $min:expr, $max:expr, $default:expr) => {
paste::paste! {
$(#[$meta])*
#[derive(Debug, Clone)]
struct [<$pattern $name>];
impl $crate::pattern::params::PatternParam for [<$pattern $name>] {
fn name(&self) -> &'static str { $cli_name }
fn description(&self) -> &'static str { $desc }
fn param_type(&self) -> $crate::pattern::params::ParamType {
$crate::pattern::params::ParamType::Number { min: $min, max: $max }
}
fn default_value(&self) -> String { $default.to_string() }
fn validate(&self, value: &str) -> Result<(), String> {
let val = value.parse::<f64>().map_err(|_| "Invalid number".to_string())?;
if !($min..=$max).contains(&val) {
return Err(format!("{} must be between {} and {}", self.name(), $min, $max));
}
Ok(())
}
fn parse(&self, _: &str) -> Result<Box<dyn $crate::pattern::params::PatternParam>, String> {
unimplemented!("Individual parameters don't support parsing")
}
fn clone_param(&self) -> Box<dyn $crate::pattern::params::PatternParam> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any { self }
}
}
};
($(#[$meta:meta])* bool $pattern:ident, $name:ident, $cli_name:expr, $desc:expr, $default:expr) => {
paste::paste! {
$(#[$meta])*
#[derive(Debug, Clone)]
struct [<$pattern $name>];
impl $crate::pattern::params::PatternParam for [<$pattern $name>] {
fn name(&self) -> &'static str { $cli_name }
fn description(&self) -> &'static str { $desc }
fn param_type(&self) -> $crate::pattern::params::ParamType {
$crate::pattern::params::ParamType::Boolean
}
fn default_value(&self) -> String { $default.to_string() }
fn validate(&self, value: &str) -> Result<(), String> {
match value {
"true" | "false" => Ok(()),
_ => Err(format!("{} must be true or false", self.name())),
}
}
fn parse(&self, _: &str) -> Result<Box<dyn $crate::pattern::params::PatternParam>, String> {
unimplemented!("Individual parameters don't support parsing")
}
fn clone_param(&self) -> Box<dyn $crate::pattern::params::PatternParam> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any { self }
}
}
};
($(#[$meta:meta])* enum $pattern:ident, $name:ident, $cli_name:expr, $desc:expr, $options:expr, $default:expr) => {
paste::paste! {
$(#[$meta])*
#[derive(Debug, Clone)]
struct [<$pattern $name>];
impl $crate::pattern::params::PatternParam for [<$pattern $name>] {
fn name(&self) -> &'static str { $cli_name }
fn description(&self) -> &'static str { $desc }
fn param_type(&self) -> $crate::pattern::params::ParamType {
$crate::pattern::params::ParamType::Enum { options: $options }
}
fn default_value(&self) -> String { $default.to_string() }
fn validate(&self, value: &str) -> Result<(), String> {
if $options.contains(&value) {
Ok(())
} else {
Err(format!("{} must be one of: {:?}", self.name(), $options))
}
}
fn parse(&self, _: &str) -> Result<Box<dyn $crate::pattern::params::PatternParam>, String> {
unimplemented!("Individual parameters don't support parsing")
}
fn clone_param(&self) -> Box<dyn $crate::pattern::params::PatternParam> {
Box::new(self.clone())
}
fn as_any(&self) -> &dyn std::any::Any { self }
}
}
};
($(#[$meta:meta])* num $pattern:ident, $name:ident, $desc:expr, $min:expr, $max:expr, $default:expr) => {
define_param!($(#[$meta])* num $pattern, $name, stringify!($name), $desc, $min, $max, $default);
};
($(#[$meta:meta])* bool $pattern:ident, $name:ident, $desc:expr, $default:expr) => {
define_param!($(#[$meta])* bool $pattern, $name, stringify!($name), $desc, $default);
};
($(#[$meta:meta])* enum $pattern:ident, $name:ident, $desc:expr, $options:expr, $default:expr) => {
define_param!($(#[$meta])* enum $pattern, $name, stringify!($name), $desc, $options, $default);
};
(@validate_composite $self:expr, $value:expr, $valid_params:expr, $($param:expr),*) => {{
if $value.contains(',') {
for part in $value.split(',') {
$self.validate(part.trim())?;
}
return Ok(());
}
let kv: Vec<&str> = $value.split('=').collect();
if kv.len() != 2 {
return Err("Parameter must be in format key=value".to_string());
}
if !$valid_params.contains(&kv[0]) {
return Err(format!("Invalid parameter name: {}", kv[0]));
}
match kv[0] {
$(
param_name if param_name == $param.name() => $param.validate(kv[1]),
)*
_ => unreachable!(), }
}};
(validate $pattern:ident, $($param_const:ident: $param_type:ty),*) => {
impl $pattern {
fn validate_params(&self, value: &str) -> Result<(), String> {
let valid_params = [$(Self::$param_const.name()),*];
define_param!(@validate_composite self, value, &valid_params, $(Self::$param_const),*)
}
}
};
}
pub use define_param as define_param_type;