Skip to main content

acorn/analyzer/
error.rs

1//! Error module for handling check errors
2//!
3use super::readability::ReadabilityType;
4use crate::analyzer::vale::ValeOutputItem;
5use crate::prelude::HashMap;
6use crate::util::ToMarkdown;
7use bon::Builder;
8use core::fmt;
9use derive_more::Display;
10use serde_json::Value;
11use validator::{ValidationError, ValidationErrorsKind};
12
13/// Trait for types that can provide a locator string indicating where an issue is located
14pub trait Location {
15    /// Return a string indicating where the issue is located, for use in check titles, summaries, etc.
16    fn locator(&self) -> String;
17    /// Return source text
18    fn source_text(&self) -> String;
19}
20/// Augmented list of validator crate error codes, with custom codes for ACORN-specific validation errors
21#[derive(Clone, Debug, Display)]
22#[display(rename_all = "lowercase")]
23pub enum ErrorCode {
24    /// Invalid ARK identifier
25    Ark,
26    /// Invalid ISO 8601 date
27    Date,
28    /// Invalid DOI
29    Doi,
30    /// Invalid email address
31    Email,
32    /// Invalid Unix epoch timestamp
33    Epoch,
34    /// Unsupported image file extension
35    Image,
36    /// Invalid IPv6 address
37    Ip6,
38    /// Invalid ISBN
39    Isbn,
40    /// Invalid kebab-case string
41    KebabCase,
42    /// Invalid latitude
43    Latitude,
44    /// Invalid longitude
45    Longitude,
46    /// Value exceeds allowed length
47    Length,
48    /// Invalid ORCiD
49    Orcid,
50    /// Other
51    Other,
52    /// Invalid patent identifier
53    Patent,
54    /// Invalid phone number
55    Phone,
56    /// Invalid polygon
57    Polygon,
58    /// Invalid RAiD identifier
59    Raid,
60    /// Value outside allowed range
61    Range,
62    /// Invalid ROR identifier
63    Ror,
64    /// Research activity prose section (see [`schema::research_activity::Sections`])
65    Section,
66    /// Invalid URL
67    Url,
68    /// Invalid ISO 8601 year
69    Year,
70}
71/// Error kind
72#[derive(Clone, Debug)]
73pub enum ErrorKind {
74    /// Readability issue where calculated index exceeds threshold of associated metric
75    Readability((f64, ReadabilityType)),
76    /// Prose issue found by Vale
77    Vale(Vec<ValeOutputItem>),
78    /// Schema validation issue found by [validator crate]
79    ///
80    /// [validator crate]: https://crates.io/crates/validator
81    Validator(ValidationErrorsKind),
82}
83/// Flattened schema validation issue.
84#[derive(Builder, Clone, Debug)]
85#[builder(start_fn = init, on(String, into))]
86pub struct ValidatorIssue {
87    pub(crate) code: ErrorCode,
88    pub(crate) path: Option<String>,
89    pub(crate) message: String,
90    pub(crate) params: HashMap<String, Value>,
91}
92impl fmt::Display for ErrorKind {
93    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94        write!(f, "{:#?}", self)
95    }
96}
97impl From<&str> for ErrorCode {
98    fn from(code: &str) -> Self {
99        match code.to_lowercase().as_str() {
100            | "ark" => Self::Ark,
101            | "iso 8601 date" | "date" => Self::Date,
102            | "doi" => Self::Doi,
103            | "email" => Self::Email,
104            | "epoch" | "unix epoch" => Self::Epoch,
105            | "image" => Self::Image,
106            | "ip6" => Self::Ip6,
107            | "isbn" => Self::Isbn,
108            | "kebabcase" => Self::KebabCase,
109            | "latitude" => Self::Latitude,
110            | "longitude" => Self::Longitude,
111            | "length" => Self::Length,
112            | "orcid" => Self::Orcid,
113            | "patent" => Self::Patent,
114            | "phone" => Self::Phone,
115            | "polygon" => Self::Polygon,
116            | "raid" | "raìd" => Self::Raid,
117            | "range" => Self::Range,
118            | "ror" => Self::Ror,
119            | "section" | "sections.approach" | "sections.areas" | "sections.capabilities" | "sections.impact" => Self::Section,
120            | "url" | "urls" => Self::Url,
121            | "year" => Self::Year,
122            | _ => Self::Other,
123        }
124    }
125}
126impl ErrorKind {
127    /// Process errors to a list of `(code, path, message, params)` tuples.
128    pub fn process(self, prefix: &str) -> Vec<ValidatorIssue> {
129        match self {
130            | ErrorKind::Validator(kind) => process(prefix, &kind),
131            | _ => vec![],
132        }
133    }
134}
135impl Location for ValidatorIssue {
136    /// Return title-friendly locator — "where to find the issue"
137    fn locator(&self) -> String {
138        let value = match self.path.as_deref() {
139            | Some(value) => match self.params.get("index") {
140                | Some(Value::Number(index)) => format!("{}[{}]", value, index),
141                | None | Some(_) => value.to_string(),
142            },
143            | None => self.code.to_string().to_uppercase(),
144        };
145        value.replace("r#", "")
146    }
147    /// Return issue source text used by schema highlighting
148    fn source_text(&self) -> String {
149        match self.params.get("value") {
150            | Some(Value::Array(items)) => items.iter().map(|item| item.to_string()).collect::<Vec<String>>().to_markdown(),
151            | Some(value) => value.to_string(),
152            | None => " ".to_string(),
153        }
154    }
155}
156/// Process validator errors into a more manageable form
157pub fn process(path: &str, kind: &ValidationErrorsKind) -> Vec<ValidatorIssue> {
158    let is_wrapper_field = |value: &str| matches!(value, "license" | "month" | "year" | "postal_code");
159    match kind {
160        | ValidationErrorsKind::Field(errors) => {
161            let result = errors
162                .iter()
163                .map(|error| {
164                    let ValidationError { code, message, params } = error.clone();
165                    let path = if path.is_empty() { None } else { Some(path.to_string()) };
166                    let message = message.map(|m| m.to_string()).unwrap_or_else(|| code.to_string());
167                    let params = params
168                        .into_iter()
169                        .map(|(key, value)| (key.to_string(), value))
170                        .collect::<HashMap<String, Value>>();
171                    ValidatorIssue::init()
172                        .code(ErrorCode::from(code.as_ref()))
173                        .maybe_path(path)
174                        .message(message)
175                        .params(params)
176                        .build()
177                })
178                .collect::<Vec<ValidatorIssue>>();
179            result
180        }
181        | ValidationErrorsKind::Struct(errors) => {
182            let result: Vec<ValidatorIssue> = errors
183                .clone()
184                .into_errors()
185                .into_iter()
186                .flat_map(|(field, kind)| {
187                    let is_wrapper_recursion = matches!(kind, ValidationErrorsKind::Field(_)) && path == field && is_wrapper_field(field.as_ref());
188                    let next_path = match (path.is_empty(), field.is_empty(), is_wrapper_recursion) {
189                        | (_, true, _) => path.to_string(),
190                        | (true, false, _) => field.to_string(),
191                        | (false, false, true) => path.to_string(),
192                        | (false, false, false) => format!("{path}.{field}"),
193                    };
194                    process(&next_path, &kind)
195                })
196                .collect();
197            result
198        }
199        | ValidationErrorsKind::List(errors) => {
200            let result: Vec<ValidatorIssue> = errors
201                .iter()
202                .flat_map(|(index, error)| {
203                    let prefix = if path.is_empty() { "".to_string() } else { path.to_string() };
204                    let kind = ValidationErrorsKind::Struct(error.clone());
205                    process(&format!("{prefix}[{index}]"), &kind)
206                })
207                .collect();
208            result
209        }
210    }
211}