use std::{collections::HashSet, fs, path::Path};
use masterror::AppError;
use serde::{Deserialize, Serialize};
use crate::error::{ConfigError, ConfigValidationError, FileReadError};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassificationConfig {
#[serde(default = "default_test_features")]
pub test_features: Vec<String>,
#[serde(default = "default_test_paths")]
pub test_paths: Vec<String>,
#[serde(default)]
pub ignore_paths: Vec<String>,
#[serde(default)]
pub ignored_authors: Vec<String>,
}
impl Default for ClassificationConfig {
fn default() -> Self {
Self {
test_features: default_test_features(),
test_paths: default_test_paths(),
ignore_paths: Vec::new(),
ignored_authors: Vec::new(),
}
}
}
fn default_test_features() -> Vec<String> {
vec![
"test-utils".to_string(),
"testing".to_string(),
"mock".to_string(),
]
}
fn default_test_paths() -> Vec<String> {
vec![
"tests/".to_string(),
"benches/".to_string(),
"examples/".to_string(),
]
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WeightsConfig {
#[serde(default = "default_public_function_weight")]
pub public_function: usize,
#[serde(default = "default_private_function_weight")]
pub private_function: usize,
#[serde(default = "default_public_struct_weight")]
pub public_struct: usize,
#[serde(default = "default_private_struct_weight")]
pub private_struct: usize,
#[serde(default = "default_impl_weight")]
pub impl_block: usize,
#[serde(default = "default_trait_weight")]
pub trait_definition: usize,
#[serde(default = "default_const_weight")]
pub const_static: usize,
}
impl Default for WeightsConfig {
fn default() -> Self {
Self {
public_function: default_public_function_weight(),
private_function: default_private_function_weight(),
public_struct: default_public_struct_weight(),
private_struct: default_private_struct_weight(),
impl_block: default_impl_weight(),
trait_definition: default_trait_weight(),
const_static: default_const_weight(),
}
}
}
fn default_public_function_weight() -> usize {
3
}
fn default_private_function_weight() -> usize {
1
}
fn default_public_struct_weight() -> usize {
3
}
fn default_private_struct_weight() -> usize {
1
}
fn default_impl_weight() -> usize {
2
}
fn default_trait_weight() -> usize {
4
}
fn default_const_weight() -> usize {
1
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct PerTypeLimits {
pub functions: Option<usize>,
pub structs: Option<usize>,
pub enums: Option<usize>,
pub traits: Option<usize>,
pub impl_blocks: Option<usize>,
pub consts: Option<usize>,
pub statics: Option<usize>,
pub type_aliases: Option<usize>,
pub macros: Option<usize>,
pub modules: Option<usize>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LimitsConfig {
#[serde(default = "default_max_prod_units")]
pub max_prod_units: usize,
#[serde(default = "default_max_weighted_score")]
pub max_weighted_score: usize,
#[serde(default)]
pub max_prod_lines: Option<usize>,
#[serde(default)]
pub per_type: Option<PerTypeLimits>,
#[serde(default = "default_fail_on_exceed")]
pub fail_on_exceed: bool,
}
impl Default for LimitsConfig {
fn default() -> Self {
Self {
max_prod_units: default_max_prod_units(),
max_weighted_score: default_max_weighted_score(),
max_prod_lines: None,
per_type: None,
fail_on_exceed: default_fail_on_exceed(),
}
}
}
fn default_max_prod_units() -> usize {
30
}
fn default_max_weighted_score() -> usize {
100
}
fn default_fail_on_exceed() -> bool {
true
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum OutputFormat {
#[default]
Github,
Json,
Human,
Comment,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OutputConfig {
#[serde(default)]
pub format: OutputFormat,
#[serde(default = "default_include_details")]
pub include_details: bool,
}
impl Default for OutputConfig {
fn default() -> Self {
Self {
format: OutputFormat::default(),
include_details: default_include_details(),
}
}
}
fn default_include_details() -> bool {
true
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct Config {
#[serde(default)]
pub classification: ClassificationConfig,
#[serde(default)]
pub weights: WeightsConfig,
#[serde(default)]
pub limits: LimitsConfig,
#[serde(default)]
pub output: OutputConfig,
}
impl Config {
pub fn from_file(path: &Path) -> Result<Self, AppError> {
let content =
fs::read_to_string(path).map_err(|e| AppError::from(FileReadError::new(path, e)))?;
toml::from_str(&content).map_err(|e| AppError::from(ConfigError::new(path, e.to_string())))
}
pub fn validate(&self) -> Result<(), AppError> {
if self.limits.max_prod_units == 0 {
return Err(ConfigValidationError {
field: "limits.max_prod_units".to_string(),
message: "must be greater than 0".to_string(),
}
.into());
}
if self.limits.max_weighted_score == 0 {
return Err(ConfigValidationError {
field: "limits.max_weighted_score".to_string(),
message: "must be greater than 0".to_string(),
}
.into());
}
let mut seen = std::collections::HashSet::new();
for author in &self.classification.ignored_authors {
if author.is_empty() {
return Err(ConfigValidationError {
field: "classification.ignored_authors".to_string(),
message: "author cannot be empty".to_string(),
}
.into());
}
if !seen.insert(author) {
return Err(ConfigValidationError {
field: "classification.ignored_authors".to_string(),
message: format!("duplicate author: {}", author),
}
.into());
}
}
Ok(())
}
pub fn test_features_set(&self) -> HashSet<&str> {
self.classification
.test_features
.iter()
.map(|s| s.as_str())
.collect()
}
pub fn should_ignore(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
self.classification
.ignore_paths
.iter()
.any(|p| path_str.contains(p))
}
pub fn should_ignore_author(&self, author: &str) -> bool {
self.classification
.ignored_authors
.iter()
.any(|ignored| author.contains(ignored) || ignored == author)
}
pub fn should_ignore_commit(&self, author: &str) -> bool {
self.should_ignore_author(author)
}
pub fn is_test_path(&self, path: &Path) -> bool {
let path_str = path.to_string_lossy();
self.classification
.test_paths
.iter()
.any(|p| path_str.contains(p))
}
pub fn is_build_script(&self, path: &Path) -> bool {
path.file_name().map(|n| n == "build.rs").unwrap_or(false)
}
}
#[derive(Debug, Default)]
pub struct ConfigBuilder {
config: Config,
}
impl ConfigBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn output_format(mut self, format: OutputFormat) -> Self {
self.config.output.format = format;
self
}
pub fn max_prod_units(mut self, limit: usize) -> Self {
self.config.limits.max_prod_units = limit;
self
}
pub fn max_weighted_score(mut self, limit: usize) -> Self {
self.config.limits.max_weighted_score = limit;
self
}
pub fn fail_on_exceed(mut self, fail: bool) -> Self {
self.config.limits.fail_on_exceed = fail;
self
}
pub fn max_prod_lines(mut self, limit: usize) -> Self {
self.config.limits.max_prod_lines = Some(limit);
self
}
pub fn per_type_limits(mut self, limits: PerTypeLimits) -> Self {
self.config.limits.per_type = Some(limits);
self
}
pub fn add_test_feature(mut self, feature: &str) -> Self {
self.config
.classification
.test_features
.push(feature.to_string());
self
}
pub fn add_ignore_path(mut self, path: &str) -> Self {
self.config
.classification
.ignore_paths
.push(path.to_string());
self
}
pub fn add_ignored_author(mut self, author: &str) -> Self {
self.config
.classification
.ignored_authors
.push(author.to_string());
self
}
pub fn build(self) -> Config {
self.config
}
}