use regex::Regex;
use crate::{InlineConfigError, NatSpec};
use super::{remove_whitespaces, INLINE_CONFIG_PREFIX};
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum InlineConfigParserError {
#[error("'{0}' is an invalid config property")]
InvalidConfigProperty(String),
#[error("'{0}' specifies an invalid profile. Available profiles are: {1}")]
InvalidProfile(String, String),
#[error("Invalid config value for key '{0}'. Unable to parse '{1}' into an integer value")]
ParseInt(String, String),
#[error("Invalid config value for key '{0}'. Unable to parse '{1}' into a boolean value")]
ParseBool(String, String),
}
pub trait InlineConfigParser
where
Self: Clone + Default + Sized + 'static,
{
fn config_key() -> String;
fn try_merge(&self, configs: &[String]) -> Result<Option<Self>, InlineConfigParserError>;
fn validate_configs(natspec: &NatSpec) -> Result<(), InlineConfigError> {
let config_key = Self::config_key();
let configs =
natspec.config_lines().filter(|l| l.contains(&config_key)).collect::<Vec<String>>();
Self::default().try_merge(&configs).map_err(|e| {
let line = natspec.debug_context();
InlineConfigError { line, source: e }
})?;
Ok(())
}
fn get_config_overrides(config_lines: &[String]) -> Vec<(String, String)> {
let mut result: Vec<(String, String)> = vec![];
let config_key = Self::config_key();
let profile = ".*";
let prefix = format!("^{INLINE_CONFIG_PREFIX}:{profile}{config_key}\\.");
let re = Regex::new(&prefix).unwrap();
config_lines
.iter()
.map(|l| remove_whitespaces(l))
.filter(|l| re.is_match(l))
.map(|l| re.replace(&l, "").to_string())
.for_each(|line| {
let key_value = line.split('=').collect::<Vec<&str>>(); if let Some(key) = key_value.first() {
if let Some(value) = key_value.last() {
result.push((key.to_string(), value.to_string()));
}
}
});
result
}
}
pub fn validate_profiles(natspec: &NatSpec, profiles: &[String]) -> Result<(), InlineConfigError> {
for config in natspec.config_lines() {
if !profiles.iter().any(|p| config.starts_with(&format!("{INLINE_CONFIG_PREFIX}:{p}."))) {
let err_line: String = natspec.debug_context();
let profiles = format!("{profiles:?}");
Err(InlineConfigError {
source: InlineConfigParserError::InvalidProfile(config, profiles),
line: err_line,
})?
}
}
Ok(())
}
pub fn parse_config_u32(key: String, value: String) -> Result<u32, InlineConfigParserError> {
value.parse().map_err(|_| InlineConfigParserError::ParseInt(key, value))
}
pub fn parse_config_bool(key: String, value: String) -> Result<bool, InlineConfigParserError> {
value.parse().map_err(|_| InlineConfigParserError::ParseBool(key, value))
}
#[cfg(test)]
mod tests {
use crate::{inline::conf_parser::validate_profiles, NatSpec};
#[test]
fn can_reject_invalid_profiles() {
let profiles = ["ci".to_string(), "default".to_string()];
let natspec = NatSpec {
contract: Default::default(),
function: Default::default(),
line: Default::default(),
docs: r#"
forge-config: ciii.invariant.depth = 1
forge-config: default.invariant.depth = 1
"#
.into(),
};
let result = validate_profiles(&natspec, &profiles);
assert!(result.is_err());
}
#[test]
fn can_accept_valid_profiles() {
let profiles = ["ci".to_string(), "default".to_string()];
let natspec = NatSpec {
contract: Default::default(),
function: Default::default(),
line: Default::default(),
docs: r#"
forge-config: ci.invariant.depth = 1
forge-config: default.invariant.depth = 1
"#
.into(),
};
let result = validate_profiles(&natspec, &profiles);
assert!(result.is_ok());
}
}