syncable_cli/analyzer/dclint/
config.rs1use std::collections::HashMap;
9
10use crate::analyzer::dclint::types::{ConfigLevel, RuleCode, Severity};
11
12#[derive(Debug, Clone)]
14pub struct RuleConfig {
15 pub level: ConfigLevel,
17 pub options: HashMap<String, serde_json::Value>,
19}
20
21impl Default for RuleConfig {
22 fn default() -> Self {
23 Self {
24 level: ConfigLevel::Error,
25 options: HashMap::new(),
26 }
27 }
28}
29
30impl RuleConfig {
31 pub fn with_level(level: ConfigLevel) -> Self {
33 Self {
34 level,
35 options: HashMap::new(),
36 }
37 }
38
39 pub fn off() -> Self {
41 Self::with_level(ConfigLevel::Off)
42 }
43
44 pub fn warn() -> Self {
46 Self::with_level(ConfigLevel::Warn)
47 }
48
49 pub fn error() -> Self {
51 Self::with_level(ConfigLevel::Error)
52 }
53
54 pub fn with_option(mut self, key: impl Into<String>, value: serde_json::Value) -> Self {
56 self.options.insert(key.into(), value);
57 self
58 }
59
60 pub fn get_option(&self, key: &str) -> Option<&serde_json::Value> {
62 self.options.get(key)
63 }
64
65 pub fn get_bool_option(&self, key: &str, default: bool) -> bool {
67 self.options
68 .get(key)
69 .and_then(|v| v.as_bool())
70 .unwrap_or(default)
71 }
72
73 pub fn get_string_option(&self, key: &str) -> Option<&str> {
75 self.options.get(key).and_then(|v| v.as_str())
76 }
77
78 pub fn get_string_array_option(&self, key: &str) -> Vec<String> {
80 self.options
81 .get(key)
82 .and_then(|v| v.as_array())
83 .map(|arr| {
84 arr.iter()
85 .filter_map(|v| v.as_str().map(String::from))
86 .collect()
87 })
88 .unwrap_or_default()
89 }
90}
91
92#[derive(Debug, Clone)]
94pub struct DclintConfig {
95 pub rules: HashMap<String, RuleConfig>,
97 pub quiet: bool,
99 pub debug: bool,
101 pub exclude: Vec<String>,
103 pub threshold: Severity,
105 pub disable_ignore_pragma: bool,
107 pub fixable_only: bool,
109}
110
111impl Default for DclintConfig {
112 fn default() -> Self {
113 Self {
114 rules: HashMap::new(),
115 quiet: false,
116 debug: false,
117 exclude: Vec::new(),
118 threshold: Severity::Style,
119 disable_ignore_pragma: false,
120 fixable_only: false,
121 }
122 }
123}
124
125impl DclintConfig {
126 pub fn new() -> Self {
128 Self::default()
129 }
130
131 pub fn with_quiet(mut self, quiet: bool) -> Self {
133 self.quiet = quiet;
134 self
135 }
136
137 pub fn with_debug(mut self, debug: bool) -> Self {
139 self.debug = debug;
140 self
141 }
142
143 pub fn with_exclude(mut self, pattern: impl Into<String>) -> Self {
145 self.exclude.push(pattern.into());
146 self
147 }
148
149 pub fn with_excludes(mut self, patterns: Vec<String>) -> Self {
151 self.exclude = patterns;
152 self
153 }
154
155 pub fn with_threshold(mut self, threshold: Severity) -> Self {
157 self.threshold = threshold;
158 self
159 }
160
161 pub fn with_rule(mut self, rule: impl Into<String>, config: RuleConfig) -> Self {
163 self.rules.insert(rule.into(), config);
164 self
165 }
166
167 pub fn ignore(mut self, rule: impl Into<String>) -> Self {
169 self.rules.insert(rule.into(), RuleConfig::off());
170 self
171 }
172
173 pub fn warn(mut self, rule: impl Into<String>) -> Self {
175 self.rules.insert(rule.into(), RuleConfig::warn());
176 self
177 }
178
179 pub fn error(mut self, rule: impl Into<String>) -> Self {
181 self.rules.insert(rule.into(), RuleConfig::error());
182 self
183 }
184
185 pub fn with_disable_ignore_pragma(mut self, disable: bool) -> Self {
187 self.disable_ignore_pragma = disable;
188 self
189 }
190
191 pub fn is_rule_ignored(&self, code: &RuleCode) -> bool {
193 self.rules
194 .get(code.as_str())
195 .map(|c| c.level == ConfigLevel::Off)
196 .unwrap_or(false)
197 }
198
199 pub fn get_rule_config(&self, code: &str) -> Option<&RuleConfig> {
201 self.rules.get(code)
202 }
203
204 pub fn effective_severity(&self, code: &RuleCode, default: Severity) -> Severity {
206 self.rules
207 .get(code.as_str())
208 .and_then(|c| c.level.to_severity())
209 .unwrap_or(default)
210 }
211
212 pub fn should_report(&self, severity: Severity) -> bool {
214 severity >= self.threshold
215 }
216
217 pub fn is_excluded(&self, path: &str) -> bool {
219 for pattern in &self.exclude {
220 if pattern.contains('*') {
222 let pattern_regex = pattern.replace('.', "\\.").replace('*', ".*");
223 if let Ok(re) = regex::Regex::new(&format!("^{}$", pattern_regex))
224 && re.is_match(path)
225 {
226 return true;
227 }
228 } else if path.contains(pattern) {
229 return true;
230 }
231 }
232 false
233 }
234}
235
236pub struct DclintConfigBuilder {
238 config: DclintConfig,
239}
240
241impl DclintConfigBuilder {
242 pub fn new() -> Self {
243 Self {
244 config: DclintConfig::default(),
245 }
246 }
247
248 pub fn from_json(mut self, json: &serde_json::Value) -> Self {
250 if let Some(rules) = json.get("rules").and_then(|v| v.as_object()) {
251 for (name, value) in rules {
252 let rule_config = match value {
253 serde_json::Value::Number(n) => {
255 if let Some(level) = n.as_u64().and_then(|n| ConfigLevel::from_u8(n as u8))
256 {
257 RuleConfig::with_level(level)
258 } else {
259 continue;
260 }
261 }
262 serde_json::Value::Array(arr) => {
264 let level = arr
265 .first()
266 .and_then(|v| v.as_u64())
267 .and_then(|n| ConfigLevel::from_u8(n as u8))
268 .unwrap_or(ConfigLevel::Error);
269
270 let mut config = RuleConfig::with_level(level);
271
272 if let Some(opts) = arr.get(1).and_then(|v| v.as_object()) {
273 for (k, v) in opts {
274 config.options.insert(k.clone(), v.clone());
275 }
276 }
277
278 config
279 }
280 _ => continue,
281 };
282
283 self.config.rules.insert(name.clone(), rule_config);
284 }
285 }
286
287 if let Some(quiet) = json.get("quiet").and_then(|v| v.as_bool()) {
288 self.config.quiet = quiet;
289 }
290
291 if let Some(debug) = json.get("debug").and_then(|v| v.as_bool()) {
292 self.config.debug = debug;
293 }
294
295 if let Some(exclude) = json.get("exclude").and_then(|v| v.as_array()) {
296 self.config.exclude = exclude
297 .iter()
298 .filter_map(|v| v.as_str().map(String::from))
299 .collect();
300 }
301
302 self
303 }
304
305 pub fn build(self) -> DclintConfig {
307 self.config
308 }
309}
310
311impl Default for DclintConfigBuilder {
312 fn default() -> Self {
313 Self::new()
314 }
315}
316
317#[cfg(test)]
318mod tests {
319 use super::*;
320
321 #[test]
322 fn test_default_config() {
323 let config = DclintConfig::default();
324 assert!(!config.quiet);
325 assert!(!config.debug);
326 assert!(config.exclude.is_empty());
327 assert!(config.rules.is_empty());
328 }
329
330 #[test]
331 fn test_rule_config() {
332 let config = DclintConfig::default()
333 .ignore("DCL001")
334 .warn("DCL002")
335 .error("DCL003");
336
337 assert!(config.is_rule_ignored(&RuleCode::new("DCL001")));
338 assert!(!config.is_rule_ignored(&RuleCode::new("DCL002")));
339 assert!(!config.is_rule_ignored(&RuleCode::new("DCL003")));
340 assert!(!config.is_rule_ignored(&RuleCode::new("DCL004"))); }
342
343 #[test]
344 fn test_effective_severity() {
345 let config = DclintConfig::default().warn("DCL001").error("DCL002");
346
347 assert_eq!(
348 config.effective_severity(&RuleCode::new("DCL001"), Severity::Error),
349 Severity::Warning
350 );
351 assert_eq!(
352 config.effective_severity(&RuleCode::new("DCL002"), Severity::Warning),
353 Severity::Error
354 );
355 assert_eq!(
357 config.effective_severity(&RuleCode::new("DCL003"), Severity::Info),
358 Severity::Info
359 );
360 }
361
362 #[test]
363 fn test_threshold() {
364 let config = DclintConfig::default().with_threshold(Severity::Warning);
365
366 assert!(config.should_report(Severity::Error));
367 assert!(config.should_report(Severity::Warning));
368 assert!(!config.should_report(Severity::Info));
369 assert!(!config.should_report(Severity::Style));
370 }
371
372 #[test]
373 fn test_exclude_patterns() {
374 let config = DclintConfig::default()
375 .with_exclude("node_modules")
376 .with_exclude("*.test.yml");
377
378 assert!(config.is_excluded("path/to/node_modules/file.yml"));
379 assert!(config.is_excluded("docker-compose.test.yml"));
380 assert!(!config.is_excluded("docker-compose.yml"));
381 }
382
383 #[test]
384 fn test_rule_options() {
385 let rule_config = RuleConfig::default()
386 .with_option("checkPullPolicy", serde_json::json!(true))
387 .with_option("pattern", serde_json::json!("^[a-z]+$"));
388
389 assert!(rule_config.get_bool_option("checkPullPolicy", false));
390 assert_eq!(rule_config.get_string_option("pattern"), Some("^[a-z]+$"));
391 assert!(rule_config.get_bool_option("nonexistent", false) == false);
392 }
393
394 #[test]
395 fn test_config_from_json() {
396 let json = serde_json::json!({
397 "rules": {
398 "no-build-and-image": 2,
399 "no-version-field": [1, { "allowEmpty": true }],
400 "services-alphabetical-order": 0
401 },
402 "quiet": true,
403 "exclude": ["*.test.yml"]
404 });
405
406 let config = DclintConfigBuilder::new().from_json(&json).build();
407
408 assert!(config.quiet);
409 assert_eq!(config.exclude, vec!["*.test.yml"]);
410
411 let rule1 = config.get_rule_config("no-build-and-image").unwrap();
412 assert_eq!(rule1.level, ConfigLevel::Error);
413
414 let rule2 = config.get_rule_config("no-version-field").unwrap();
415 assert_eq!(rule2.level, ConfigLevel::Warn);
416 assert!(rule2.get_bool_option("allowEmpty", false));
417
418 let rule3 = config
419 .get_rule_config("services-alphabetical-order")
420 .unwrap();
421 assert_eq!(rule3.level, ConfigLevel::Off);
422 }
423}