Skip to main content

rustquty_core/
config.rs

1//! Configuration file parsing (`rustquty.toml`).
2
3use serde::{Deserialize, Serialize};
4use std::path::Path;
5
6#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
7#[serde(rename_all = "kebab-case")]
8pub struct Config {
9    #[serde(default)]
10    pub profile: ConfigProfile,
11    #[serde(default)]
12    pub collectors: ConfigCollectors,
13    #[serde(default)]
14    pub gate: ConfigGate,
15    #[serde(default)]
16    pub output: ConfigOutput,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
20#[serde(rename_all = "kebab-case")]
21pub struct ConfigProfile {
22    #[serde(default = "default_profile_default")]
23    pub default: String,
24}
25
26fn default_profile_default() -> String {
27    "full".to_string()
28}
29
30impl Default for ConfigProfile {
31    fn default() -> Self {
32        Self {
33            default: default_profile_default(),
34        }
35    }
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
39#[serde(rename_all = "kebab-case")]
40pub struct ConfigCollectors {
41    #[serde(default)]
42    pub mutants: Option<bool>,
43    #[serde(default)]
44    pub hack: Option<bool>,
45    #[serde(default)]
46    pub coverage: Option<bool>,
47    #[serde(default)]
48    pub deny: Option<bool>,
49    #[serde(default)]
50    pub audit: Option<bool>,
51}
52
53#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
54#[serde(rename_all = "kebab-case")]
55pub struct ConfigGate {
56    #[serde(default)]
57    pub coverage: Option<ConfigGateCoverage>,
58    #[serde(default)]
59    pub size: Option<SizeConfig>,
60    #[serde(default)]
61    pub complexity: Option<ComplexityConfig>,
62    #[serde(default)]
63    pub defaults: Option<ConfigGateDefaults>,
64}
65
66#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
67pub struct ConfigGateCoverage {
68    #[serde(default)]
69    pub min_line_percent: Option<f64>,
70}
71
72/// Absolute thresholds based on industry standards (SonarQube, ESLint, DeepSource).
73/// When present, these override the ratchet model for the specified metrics.
74#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
75#[serde(rename_all = "kebab-case")]
76pub struct ConfigGateDefaults {
77    /// Maximum cyclomatic complexity per function (SonarQube default: 15, DeepSource: 10).
78    #[serde(default)]
79    pub max_cyclomatic_per_function: Option<u32>,
80    /// Maximum nesting depth per function (ESLint default: 4, Detekt/ReSharper: 5).
81    #[serde(default)]
82    pub max_nesting_depth: Option<u32>,
83    /// Maximum lines per function (SonarQube default: 80, ESLint: 50, Detekt: 60).
84    #[serde(default)]
85    pub max_lines_per_function: Option<u32>,
86    /// Maximum lines per file (SonarQube default: 1000).
87    #[serde(default)]
88    pub max_lines_per_file: Option<u32>,
89    /// Maximum code lines per file (non-comment, non-blank).
90    #[serde(default)]
91    pub max_code_lines_per_file: Option<u32>,
92    /// Maximum parameters per function (SonarQube default: 7, Detekt: 6, ESLint: 3).
93    #[serde(default)]
94    pub max_parameters_per_function: Option<u32>,
95    /// Minimum line coverage percent (SonarQube default: 80.0).
96    #[serde(default)]
97    pub min_coverage_percent: Option<f64>,
98    /// Maximum duplicate lines (SonarQube default: 3% of new code).
99    #[serde(default)]
100    pub max_duplicate_lines: Option<u32>,
101    /// Maximum clippy warnings.
102    #[serde(default)]
103    pub max_clippy_warnings: Option<u32>,
104    /// Maximum line length in characters (ESLint default: 80, rustfmt default: 120).
105    #[serde(default)]
106    pub max_line_length: Option<usize>,
107}
108
109/// Configuration for the size gate, loaded from [gate.size] in TOML.
110#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
111#[serde(rename_all = "kebab-case")]
112pub struct SizeConfig {
113    /// Maximum total lines per file.
114    #[serde(default)]
115    pub max_lines_per_file: Option<u32>,
116    /// Maximum code lines per file (non-comment, non-blank).
117    #[serde(default)]
118    pub max_code_lines_per_file: Option<u32>,
119    /// Maximum lines per function.
120    #[serde(default)]
121    pub max_lines_per_function: Option<u32>,
122    /// Maximum parameters per function.
123    #[serde(default)]
124    pub max_parameters_per_function: Option<u32>,
125}
126
127/// Configuration for the complexity gate, loaded from [gate.complexity] in TOML.
128#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
129#[serde(rename_all = "kebab-case")]
130pub struct ComplexityConfig {
131    /// Maximum cyclomatic complexity per function.
132    #[serde(default)]
133    pub max_cyclomatic_per_function: Option<u32>,
134    /// Maximum nesting depth per function.
135    #[serde(default)]
136    pub max_nesting_depth: Option<u32>,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
140#[serde(rename_all = "kebab-case")]
141pub struct ConfigOutput {
142    #[serde(default)]
143    pub dir: Option<String>,
144}
145
146impl Config {
147    /// Load `rustquty.toml` from the given directory.
148    pub fn load(path: &Path) -> anyhow::Result<Self> {
149        let contents = std::fs::read_to_string(path)?;
150        let config: Config = toml::from_str(&contents)?;
151        Ok(config)
152    }
153}
154
155#[cfg(test)]
156mod tests {
157    use super::*;
158
159    const EXAMPLE_CONFIG: &str = r#"
160[profile]
161default = "deep"
162
163[collectors]
164mutants = false
165
166[gate.coverage]
167min_line_percent = 80.0
168
169[output]
170dir = "quality"
171"#;
172
173    #[test]
174    fn test_parse_config() {
175        let config: Config = toml::from_str(EXAMPLE_CONFIG).unwrap();
176        assert_eq!(config.profile.default, "deep");
177        assert_eq!(config.collectors.mutants, Some(false));
178        assert_eq!(
179            config.gate.coverage.as_ref().unwrap().min_line_percent,
180            Some(80.0)
181        );
182        assert_eq!(config.output.dir.as_deref(), Some("quality"));
183    }
184}