use std::collections::HashMap;
use crate::analyzer::dclint::types::{ConfigLevel, RuleCode, Severity};
#[derive(Debug, Clone)]
pub struct RuleConfig {
pub level: ConfigLevel,
pub options: HashMap<String, serde_json::Value>,
}
impl Default for RuleConfig {
fn default() -> Self {
Self {
level: ConfigLevel::Error,
options: HashMap::new(),
}
}
}
impl RuleConfig {
pub fn with_level(level: ConfigLevel) -> Self {
Self {
level,
options: HashMap::new(),
}
}
pub fn off() -> Self {
Self::with_level(ConfigLevel::Off)
}
pub fn warn() -> Self {
Self::with_level(ConfigLevel::Warn)
}
pub fn error() -> Self {
Self::with_level(ConfigLevel::Error)
}
pub fn with_option(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
self.options.insert(key.into(), value);
self
}
pub fn get_option(&self, key: &str) -> Option<&serde_json::Value> {
self.options.get(key)
}
pub fn get_bool_option(&self, key: &str, default: bool) -> bool {
self.options
.get(key)
.and_then(|v| v.as_bool())
.unwrap_or(default)
}
pub fn get_string_option(&self, key: &str) -> Option<&str> {
self.options.get(key).and_then(|v| v.as_str())
}
pub fn get_string_array_option(&self, key: &str) -> Vec<String> {
self.options
.get(key)
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default()
}
}
#[derive(Debug, Clone)]
pub struct DclintConfig {
pub rules: HashMap<String, RuleConfig>,
pub quiet: bool,
pub debug: bool,
pub exclude: Vec<String>,
pub threshold: Severity,
pub disable_ignore_pragma: bool,
pub fixable_only: bool,
}
impl Default for DclintConfig {
fn default() -> Self {
Self {
rules: HashMap::new(),
quiet: false,
debug: false,
exclude: Vec::new(),
threshold: Severity::Style,
disable_ignore_pragma: false,
fixable_only: false,
}
}
}
impl DclintConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_quiet(mut self, quiet: bool) -> Self {
self.quiet = quiet;
self
}
pub fn with_debug(mut self, debug: bool) -> Self {
self.debug = debug;
self
}
pub fn with_exclude(mut self, pattern: impl Into<String>) -> Self {
self.exclude.push(pattern.into());
self
}
pub fn with_excludes(mut self, patterns: Vec<String>) -> Self {
self.exclude = patterns;
self
}
pub fn with_threshold(mut self, threshold: Severity) -> Self {
self.threshold = threshold;
self
}
pub fn with_rule(mut self, rule: impl Into<String>, config: RuleConfig) -> Self {
self.rules.insert(rule.into(), config);
self
}
pub fn ignore(mut self, rule: impl Into<String>) -> Self {
self.rules.insert(rule.into(), RuleConfig::off());
self
}
pub fn warn(mut self, rule: impl Into<String>) -> Self {
self.rules.insert(rule.into(), RuleConfig::warn());
self
}
pub fn error(mut self, rule: impl Into<String>) -> Self {
self.rules.insert(rule.into(), RuleConfig::error());
self
}
pub fn with_disable_ignore_pragma(mut self, disable: bool) -> Self {
self.disable_ignore_pragma = disable;
self
}
pub fn is_rule_ignored(&self, code: &RuleCode) -> bool {
self.rules
.get(code.as_str())
.map(|c| c.level == ConfigLevel::Off)
.unwrap_or(false)
}
pub fn get_rule_config(&self, code: &str) -> Option<&RuleConfig> {
self.rules.get(code)
}
pub fn effective_severity(&self, code: &RuleCode, default: Severity) -> Severity {
self.rules
.get(code.as_str())
.and_then(|c| c.level.to_severity())
.unwrap_or(default)
}
pub fn should_report(&self, severity: Severity) -> bool {
severity >= self.threshold
}
pub fn is_excluded(&self, path: &str) -> bool {
for pattern in &self.exclude {
if pattern.contains('*') {
let pattern_regex = pattern.replace('.', "\\.").replace('*', ".*");
if let Ok(re) = regex::Regex::new(&format!("^{}$", pattern_regex))
&& re.is_match(path)
{
return true;
}
} else if path.contains(pattern) {
return true;
}
}
false
}
}
pub struct DclintConfigBuilder {
config: DclintConfig,
}
impl DclintConfigBuilder {
pub fn new() -> Self {
Self {
config: DclintConfig::default(),
}
}
pub fn from_json(mut self, json: &serde_json::Value) -> Self {
if let Some(rules) = json.get("rules").and_then(|v| v.as_object()) {
for (name, value) in rules {
let rule_config = match value {
serde_json::Value::Number(n) => {
if let Some(level) = n.as_u64().and_then(|n| ConfigLevel::from_u8(n as u8))
{
RuleConfig::with_level(level)
} else {
continue;
}
}
serde_json::Value::Array(arr) => {
let level = arr
.first()
.and_then(|v| v.as_u64())
.and_then(|n| ConfigLevel::from_u8(n as u8))
.unwrap_or(ConfigLevel::Error);
let mut config = RuleConfig::with_level(level);
if let Some(opts) = arr.get(1).and_then(|v| v.as_object()) {
for (k, v) in opts {
config.options.insert(k.clone(), v.clone());
}
}
config
}
_ => continue,
};
self.config.rules.insert(name.clone(), rule_config);
}
}
if let Some(quiet) = json.get("quiet").and_then(|v| v.as_bool()) {
self.config.quiet = quiet;
}
if let Some(debug) = json.get("debug").and_then(|v| v.as_bool()) {
self.config.debug = debug;
}
if let Some(exclude) = json.get("exclude").and_then(|v| v.as_array()) {
self.config.exclude = exclude
.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect();
}
self
}
pub fn build(self) -> DclintConfig {
self.config
}
}
impl Default for DclintConfigBuilder {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_config() {
let config = DclintConfig::default();
assert!(!config.quiet);
assert!(!config.debug);
assert!(config.exclude.is_empty());
assert!(config.rules.is_empty());
}
#[test]
fn test_rule_config() {
let config = DclintConfig::default()
.ignore("DCL001")
.warn("DCL002")
.error("DCL003");
assert!(config.is_rule_ignored(&RuleCode::new("DCL001")));
assert!(!config.is_rule_ignored(&RuleCode::new("DCL002")));
assert!(!config.is_rule_ignored(&RuleCode::new("DCL003")));
assert!(!config.is_rule_ignored(&RuleCode::new("DCL004"))); }
#[test]
fn test_effective_severity() {
let config = DclintConfig::default().warn("DCL001").error("DCL002");
assert_eq!(
config.effective_severity(&RuleCode::new("DCL001"), Severity::Error),
Severity::Warning
);
assert_eq!(
config.effective_severity(&RuleCode::new("DCL002"), Severity::Warning),
Severity::Error
);
assert_eq!(
config.effective_severity(&RuleCode::new("DCL003"), Severity::Info),
Severity::Info
);
}
#[test]
fn test_threshold() {
let config = DclintConfig::default().with_threshold(Severity::Warning);
assert!(config.should_report(Severity::Error));
assert!(config.should_report(Severity::Warning));
assert!(!config.should_report(Severity::Info));
assert!(!config.should_report(Severity::Style));
}
#[test]
fn test_exclude_patterns() {
let config = DclintConfig::default()
.with_exclude("node_modules")
.with_exclude("*.test.yml");
assert!(config.is_excluded("path/to/node_modules/file.yml"));
assert!(config.is_excluded("docker-compose.test.yml"));
assert!(!config.is_excluded("docker-compose.yml"));
}
#[test]
fn test_rule_options() {
let rule_config = RuleConfig::default()
.with_option("checkPullPolicy", serde_json::json!(true))
.with_option("pattern", serde_json::json!("^[a-z]+$"));
assert!(rule_config.get_bool_option("checkPullPolicy", false));
assert_eq!(rule_config.get_string_option("pattern"), Some("^[a-z]+$"));
assert!(rule_config.get_bool_option("nonexistent", false) == false);
}
#[test]
fn test_config_from_json() {
let json = serde_json::json!({
"rules": {
"no-build-and-image": 2,
"no-version-field": [1, { "allowEmpty": true }],
"services-alphabetical-order": 0
},
"quiet": true,
"exclude": ["*.test.yml"]
});
let config = DclintConfigBuilder::new().from_json(&json).build();
assert!(config.quiet);
assert_eq!(config.exclude, vec!["*.test.yml"]);
let rule1 = config.get_rule_config("no-build-and-image").unwrap();
assert_eq!(rule1.level, ConfigLevel::Error);
let rule2 = config.get_rule_config("no-version-field").unwrap();
assert_eq!(rule2.level, ConfigLevel::Warn);
assert!(rule2.get_bool_option("allowEmpty", false));
let rule3 = config
.get_rule_config("services-alphabetical-order")
.unwrap();
assert_eq!(rule3.level, ConfigLevel::Off);
}
}