Skip to main content

skif_config/
rules.rs

1use indexmap::IndexMap;
2use serde::ser::Error as _;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use skif_core::Severity;
5
6/// The level a rule is configured to: `off`, `info`, `warn` or `error`.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
9#[serde(rename_all = "lowercase")]
10#[non_exhaustive]
11pub enum Level {
12    /// The rule does not run.
13    Off,
14    /// Diagnostics are informational.
15    Info,
16    /// Diagnostics are warnings.
17    Warn,
18    /// Diagnostics are errors.
19    Error,
20}
21
22impl Level {
23    /// The diagnostic severity this level maps to, or `None` when the rule is off.
24    #[must_use]
25    pub const fn severity(self) -> Option<Severity> {
26        match self {
27            Self::Off => None,
28            Self::Info => Some(Severity::Info),
29            Self::Warn => Some(Severity::Warning),
30            Self::Error => Some(Severity::Error),
31        }
32    }
33
34    pub(crate) fn parse(text: &str) -> Option<Self> {
35        match text {
36            "off" => Some(Self::Off),
37            "info" => Some(Self::Info),
38            "warn" => Some(Self::Warn),
39            "error" => Some(Self::Error),
40            _ => None,
41        }
42    }
43}
44
45/// The raw options table of one rule. The rule that owns the options defines and validates their shape.
46pub type RuleOptions = toml::Table;
47
48/// Configuration of one rule: an optional level plus options.
49///
50/// In the table form, `level` is the key that sets the level and is what is written back; `severity` is accepted as
51/// an alias. Both names are reserved: they are never passed on as rule options, so a rule cannot define an option
52/// called `level` or `severity`, and setting both in one table is an error.
53#[derive(Debug, Clone, PartialEq, Default)]
54#[non_exhaustive]
55pub struct RuleConfig {
56    /// `None` keeps the rule's default severity.
57    pub severity: Option<Level>,
58    /// Rule-specific options, exactly as written in the configuration.
59    pub options: RuleOptions,
60}
61
62impl RuleConfig {
63    /// A rule configuration that sets only the level.
64    #[must_use]
65    pub fn level(level: Level) -> Self {
66        Self {
67            severity: Some(level),
68            options: RuleOptions::new(),
69        }
70    }
71
72    /// Parses the value of `[rules] <id>`: a level string or a table.
73    pub(crate) fn from_value(id: &str, value: toml::Value) -> Result<Self, String> {
74        match value {
75            toml::Value::String(text) => Level::parse(&text).map(Self::level).ok_or_else(|| bad_level(id, &text)),
76            toml::Value::Table(mut table) => {
77                let severity = table.remove("severity");
78                let level = table.remove("level");
79                let (key, value) = match (severity, level) {
80                    (Some(_), Some(_)) => {
81                        return Err(format!(
82                            "rule \"{id}\": set either `level` or `severity`, not both; they name the same setting"
83                        ));
84                    }
85                    (Some(value), None) => ("severity", Some(value)),
86                    (None, value) => ("level", value),
87                };
88                let level = match value {
89                    None => None,
90                    Some(toml::Value::String(text)) => Some(Level::parse(&text).ok_or_else(|| bad_level(id, &text))?),
91                    Some(other) => {
92                        return Err(format!(
93                            "rule \"{id}\": {key} must be a string, found {}",
94                            other.type_str()
95                        ));
96                    }
97                };
98                Ok(Self {
99                    severity: level,
100                    options: table,
101                })
102            }
103            other => Err(format!(
104                "rule \"{id}\": expected a level string or a table, found {}",
105                other.type_str()
106            )),
107        }
108    }
109
110    fn to_value(&self) -> Result<toml::Value, toml::ser::Error> {
111        let options = self.options.clone();
112        if options.is_empty() {
113            if let Some(level) = self.severity {
114                return toml::Value::try_from(level);
115            }
116            return Ok(toml::Value::Table(options));
117        }
118        let mut table = toml::Table::new();
119        if let Some(level) = self.severity {
120            table.insert("level".to_string(), toml::Value::try_from(level)?);
121        }
122        table.extend(options);
123        Ok(toml::Value::Table(table))
124    }
125}
126
127fn bad_level(id: &str, text: &str) -> String {
128    format!("rule \"{id}\": invalid level \"{text}\", expected one of \"off\", \"info\", \"warn\", \"error\"")
129}
130
131impl Serialize for RuleConfig {
132    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
133        self.to_value().map_err(S::Error::custom)?.serialize(serializer)
134    }
135}
136
137/// Serde adapter for `[rules]`: the option shape depends on the rule ID, so it is resolved per entry.
138pub(crate) mod rules_map {
139    use super::{Deserialize, Deserializer, IndexMap, RuleConfig, Serialize, Serializer};
140    use serde::de::Error as _;
141
142    pub fn serialize<S: Serializer>(map: &IndexMap<String, RuleConfig>, serializer: S) -> Result<S::Ok, S::Error> {
143        map.serialize(serializer)
144    }
145
146    pub fn deserialize<'de, De: Deserializer<'de>>(
147        deserializer: De,
148    ) -> Result<IndexMap<String, RuleConfig>, De::Error> {
149        let raw = IndexMap::<String, toml::Value>::deserialize(deserializer)?;
150        raw.into_iter()
151            .map(|(id, value)| match RuleConfig::from_value(&id, value) {
152                Ok(config) => Ok((id, config)),
153                Err(message) => Err(De::Error::custom(message)),
154            })
155            .collect()
156    }
157}