fontspector_checkapi/
status.rs

1use std::str::FromStr;
2
3use serde::{Deserialize, Serialize};
4
5use crate::Override;
6#[derive(Debug, PartialEq, PartialOrd, Ord, Eq, Copy, Clone, Serialize, Deserialize, Hash)]
7#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
8#[serde(rename_all = "UPPERCASE")]
9/// A severity level for a single check subresult
10pub enum StatusCode {
11    /// Skip: the check didn't run because some condition was not met
12    Skip,
13    /// Pass: there's no problem here
14    Pass,
15    /// Info: the check returned some useful information, but no problems
16    Info,
17    /// Warn: a problem which should be manually reviewed
18    Warn,
19    /// Fail: a problem materially affects the correctness of the font
20    Fail,
21    /// Error: something went wrong
22    ///
23    /// An Error is when something which returns a `Result<>` gave us
24    /// an `Err`` - for example a file couldn't be found or couldn't be
25    /// parsed, even though we did our best to check for things. In
26    /// other words, it's something so bad there's no point continuing
27    /// with the check; it's equivalent to a Fontbakery FATAL.
28    Error,
29}
30
31impl FromStr for StatusCode {
32    type Err = ();
33
34    fn from_str(s: &str) -> Result<Self, Self::Err> {
35        match s {
36            "SKIP" => Ok(StatusCode::Skip),
37            "INFO" => Ok(StatusCode::Info),
38            "PASS" => Ok(StatusCode::Pass),
39            "WARN" => Ok(StatusCode::Warn),
40            "FAIL" => Ok(StatusCode::Fail),
41            "ERROR" => Ok(StatusCode::Error),
42            _ => Err(()),
43        }
44    }
45}
46
47impl StatusCode {
48    /// Return an iterator over all status codes
49    ///
50    /// Used to provide a list of possible status codes to the user when
51    /// selecting the minimum reported status.
52    pub fn all() -> impl Iterator<Item = StatusCode> {
53        vec![
54            StatusCode::Skip,
55            StatusCode::Info,
56            StatusCode::Pass,
57            StatusCode::Warn,
58            StatusCode::Fail,
59            StatusCode::Error,
60        ]
61        .into_iter()
62    }
63
64    /// Convert a string to a status code
65    ///
66    /// This is used when the status code comes from an external source,
67    /// such as FontBakery.
68    pub fn from_string(s: &str) -> Option<StatusCode> {
69        FromStr::from_str(s).ok()
70    }
71}
72impl std::fmt::Display for StatusCode {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match *self {
75            StatusCode::Pass => write!(f, "PASS"),
76            StatusCode::Skip => write!(f, "SKIP"),
77            StatusCode::Fail => write!(f, "FAIL"),
78            StatusCode::Warn => write!(f, "WARN"),
79            StatusCode::Info => write!(f, "INFO"),
80            StatusCode::Error => write!(f, "ERROR"),
81        }
82    }
83}
84#[derive(Debug, Clone, Serialize)]
85/// A status message from a check
86///
87/// This is a subresult, in the sense that a check may return multiple failures
88/// and warnings; all the subresults then get wrapped into a [crate::CheckResult]
89/// which is the final result of the check.
90pub struct Status {
91    /// A message to explain the status
92    #[serde(skip_serializing_if = "Option::is_none")]
93    pub message: Option<String>,
94    /// The severity of the status
95    pub severity: StatusCode,
96    /// A code to identify the status
97    #[serde(skip_serializing_if = "Option::is_none")]
98    pub code: Option<String>,
99    /// Additional metadata provided to the reporter
100    #[serde(skip_serializing_if = "Option::is_none")]
101    pub metadata: Option<serde_json::Value>,
102}
103
104impl std::fmt::Display for Status {
105    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
106        write!(f, "**{:}**: ", self.severity)?;
107        if let Some(code) = self.code.as_ref() {
108            write!(f, "[{}]: ", code)?;
109        }
110        if let Some(message) = self.message.as_ref() {
111            write!(f, "{:}", message)?;
112        }
113        Ok(())
114    }
115}
116
117impl Status {
118    /// Return a single pass result from a check
119    pub fn just_one_pass() -> Box<dyn Iterator<Item = Status>> {
120        Box::new(vec![Status::pass()].into_iter())
121    }
122
123    /// Return a single warn result from a check
124    pub fn just_one_warn(code: &str, message: &str) -> Box<dyn Iterator<Item = Status>> {
125        Box::new(vec![Status::warn(code, message)].into_iter())
126    }
127
128    /// Return a single info result from a check
129    pub fn just_one_info(code: &str, message: &str) -> Box<dyn Iterator<Item = Status>> {
130        Box::new(vec![Status::info(code, message)].into_iter())
131    }
132
133    /// Return a single fail result from a check
134    pub fn just_one_fail(code: &str, message: &str) -> Box<dyn Iterator<Item = Status>> {
135        Box::new(vec![Status::fail(code, message)].into_iter())
136    }
137
138    /// Return a single skip result from a check
139    pub fn just_one_skip(code: &str, message: &str) -> Box<dyn Iterator<Item = Status>> {
140        Box::new(vec![Status::skip(code, message)].into_iter())
141    }
142
143    /// Create a status with a pass severity
144    pub fn pass() -> Self {
145        Self {
146            message: None,
147            code: None,
148            severity: StatusCode::Pass,
149            metadata: None,
150        }
151    }
152    /// Create a status with a fail severity
153    pub fn fail(code: &str, message: &str) -> Self {
154        Self {
155            message: Some(message.to_string()),
156            code: Some(code.to_string()),
157            severity: StatusCode::Fail,
158            metadata: None,
159        }
160    }
161    /// Create a status with a warning severity
162    pub fn warn(code: &str, message: &str) -> Self {
163        Self {
164            message: Some(message.to_string()),
165            code: Some(code.to_string()),
166            severity: StatusCode::Warn,
167            metadata: None,
168        }
169    }
170    /// Create a status with an info severity
171    pub fn skip(code: &str, message: &str) -> Self {
172        Self {
173            message: Some(message.to_string()),
174            code: Some(code.to_string()),
175            severity: StatusCode::Skip,
176            metadata: None,
177        }
178    }
179    /// Create a status with an info severity
180    pub fn info(code: &str, message: &str) -> Self {
181        Self {
182            message: Some(message.to_string()),
183            code: Some(code.to_string()),
184            severity: StatusCode::Info,
185            metadata: None,
186        }
187    }
188    /// Create a status with an error severity
189    pub fn error(code: Option<&str>, message: &str) -> Self {
190        Self {
191            message: Some(message.to_string()),
192            code: code.map(|x| x.to_string()),
193            severity: StatusCode::Error,
194            metadata: None,
195        }
196    }
197
198    /// Apply an override to the status
199    ///
200    /// Overrides are provided by the profile or by the user's configuration file;
201    /// they are used to override the severity of a check result.
202    pub fn process_override(&mut self, overrides: &[Override]) {
203        let code = self.code.as_deref();
204        if let Some(override_) = overrides.iter().find(|x| Some(x.code.as_str()) == code) {
205            self.severity = override_.status;
206            self.message = Some(format!(
207                "{} (Overriden: {})",
208                self.message
209                    .clone()
210                    .unwrap_or("No original message".to_string()),
211                override_.reason
212            ))
213        }
214    }
215}
216
217/// Reflects the result of some kind of early return from a check function
218///
219/// This may be because there was an error, or because the check was skipped.
220#[derive(Debug)]
221pub enum CheckError {
222    /// An error occurred
223    Error(String),
224    /// The check was skipped due to an error condition
225    Skip {
226        /// Code to identify the skip reason
227        code: String,
228        /// Message to explain the skip to the user
229        message: String,
230    },
231}
232
233impl<T> From<T> for CheckError
234where
235    T: std::error::Error,
236{
237    fn from(e: T) -> Self {
238        CheckError::Error(e.to_string())
239    }
240}
241
242impl CheckError {
243    /// Return an error which skips the check
244    ///
245    /// This allows you to skip a check early if an error is raised, for example
246    /// if a particular table is missing from the font.
247    pub fn skip(code: &str, message: &str) -> Self {
248        CheckError::Skip {
249            code: code.to_string(),
250            message: message.to_string(),
251        }
252    }
253}
254
255/// A list of statuses
256pub type StatusList = Box<dyn Iterator<Item = Status>>;
257/// The expected return type of a check implementation function
258pub type CheckFnResult = Result<StatusList, CheckError>;