1use indexmap::IndexMap;
2use serde::ser::Error as _;
3use serde::{Deserialize, Deserializer, Serialize, Serializer};
4use skif_core::Severity;
5
6#[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 Off,
14 Info,
16 Warn,
18 Error,
20}
21
22impl Level {
23 #[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
45pub type RuleOptions = toml::Table;
47
48#[derive(Debug, Clone, PartialEq, Default)]
54#[non_exhaustive]
55pub struct RuleConfig {
56 pub severity: Option<Level>,
58 pub options: RuleOptions,
60}
61
62impl RuleConfig {
63 #[must_use]
65 pub fn level(level: Level) -> Self {
66 Self {
67 severity: Some(level),
68 options: RuleOptions::new(),
69 }
70 }
71
72 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
137pub(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}