Skip to main content

ontocore_diagnostics/
config.rs

1//! Workspace diagnostic rule configuration (`.ontocore/diagnostics.toml`).
2
3use ontocore_core::{DiagnosticCode, DiagnosticSeverity};
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::path::Path;
7
8#[derive(Debug, Clone, Default, Deserialize)]
9pub struct DiagnosticConfig {
10    #[serde(default)]
11    pub rules: HashMap<String, RuleConfig>,
12}
13
14#[derive(Debug, Clone, Deserialize)]
15pub struct RuleConfig {
16    #[serde(default = "default_enabled")]
17    pub enabled: bool,
18    pub severity: Option<String>,
19}
20
21fn default_enabled() -> bool {
22    true
23}
24
25impl DiagnosticConfig {
26    pub fn load(path: &Path) -> Result<Self, String> {
27        let text = std::fs::read_to_string(path)
28            .map_err(|e| format!("failed to read {}: {e}", path.display()))?;
29        toml::from_str(&text).map_err(|e| format!("invalid diagnostics config: {e}"))
30    }
31
32    pub fn is_rule_enabled(&self, code: DiagnosticCode) -> bool {
33        let key = code.as_str();
34        self.rules.get(key).map(|r| r.enabled).unwrap_or(true)
35    }
36
37    pub fn severity_override(&self, code: DiagnosticCode) -> Option<DiagnosticSeverity> {
38        let key = code.as_str();
39        let sev = self.rules.get(key)?.severity.as_deref()?;
40        match sev.to_ascii_lowercase().as_str() {
41            "error" => Some(DiagnosticSeverity::Error),
42            "warning" => Some(DiagnosticSeverity::Warning),
43            "info" | "hint" => Some(DiagnosticSeverity::Info),
44            _ => None,
45        }
46    }
47}
48
49pub fn find_config(workspace: &Path) -> Option<DiagnosticConfig> {
50    let path = workspace.join(".ontocore").join("diagnostics.toml");
51    DiagnosticConfig::load(&path).ok()
52}
53
54#[cfg(test)]
55mod tests {
56    use super::*;
57
58    #[test]
59    fn parses_rule_toggles() {
60        let cfg: DiagnosticConfig = toml::from_str(
61            r#"
62[rules.missing_label]
63enabled = false
64[rules.broken_import]
65severity = "error"
66"#,
67        )
68        .unwrap();
69        assert!(!cfg.is_rule_enabled(DiagnosticCode::MissingLabel));
70        assert_eq!(
71            cfg.severity_override(DiagnosticCode::BrokenImport),
72            Some(DiagnosticSeverity::Error)
73        );
74    }
75
76    #[test]
77    fn find_config_loads_from_workspace_dot_ontocore() {
78        let dir = tempfile::tempdir().unwrap();
79        std::fs::create_dir_all(dir.path().join(".ontocore")).unwrap();
80        std::fs::write(
81            dir.path().join(".ontocore/diagnostics.toml"),
82            "[rules.orphan_class]\nenabled = false\n",
83        )
84        .unwrap();
85        let cfg = find_config(dir.path()).expect("config");
86        assert!(!cfg.is_rule_enabled(DiagnosticCode::OrphanClass));
87    }
88
89    #[test]
90    fn hint_severity_maps_to_info() {
91        let cfg: DiagnosticConfig = toml::from_str(
92            r#"
93[rules.duplicate_label]
94severity = "hint"
95"#,
96        )
97        .unwrap();
98        assert_eq!(
99            cfg.severity_override(DiagnosticCode::DuplicateLabel),
100            Some(DiagnosticSeverity::Info)
101        );
102    }
103}