1use 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
13pub trait Location {
15 fn locator(&self) -> String;
17 fn source_text(&self) -> String;
19}
20#[derive(Clone, Debug, Display)]
22#[display(rename_all = "lowercase")]
23pub enum ErrorCode {
24 Ark,
26 Date,
28 Doi,
30 Email,
32 Epoch,
34 Image,
36 Ip6,
38 Isbn,
40 KebabCase,
42 Latitude,
44 Longitude,
46 Length,
48 Orcid,
50 Other,
52 Patent,
54 Phone,
56 Polygon,
58 Raid,
60 Range,
62 Ror,
64 Section,
66 Url,
68 Year,
70}
71#[derive(Clone, Debug)]
73pub enum ErrorKind {
74 Readability((f64, ReadabilityType)),
76 Vale(Vec<ValeOutputItem>),
78 Validator(ValidationErrorsKind),
82}
83#[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 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 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 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}
156pub 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}