1use super::readability::ReadabilityType;
4use crate::analyzer::vale::ValeOutputItem;
5use crate::io::document::{DocumentPath, DocumentQuery};
6use crate::prelude::HashMap;
7use crate::util::MarkdownSupport;
8use bon::Builder;
9use core::fmt;
10use derive_more::Display;
11use serde_json::Value;
12use validator::{ValidationError, ValidationErrorsKind};
13
14pub trait DocumentTarget {
16 fn locator(&self) -> String;
18 fn source_text(&self) -> String;
20 fn query(&self) -> DocumentQuery;
22}
23#[derive(Clone, Debug, Display)]
25#[display(rename_all = "lowercase")]
26pub enum ErrorCode {
27 Ark,
29 Date,
31 Doi,
33 Email,
35 Epoch,
37 Image,
39 Ip6,
41 Isbn,
43 KebabCase,
45 Latitude,
47 Longitude,
49 Length,
51 Orcid,
53 Other,
55 Patent,
57 Phone,
59 Polygon,
61 Raid,
63 Range,
65 Ror,
67 Section,
69 Url,
71 Year,
73}
74#[derive(Clone, Debug)]
76pub enum ErrorKind {
77 Readability((f64, ReadabilityType)),
79 Vale(Vec<ValeOutputItem>),
81 Validator(ValidationErrorsKind),
85}
86#[derive(Builder, Clone, Debug)]
88#[builder(start_fn = init, on(String, into))]
89pub struct ValidatorIssue {
90 pub(crate) code: ErrorCode,
91 pub(crate) path: Option<String>,
92 pub(crate) message: String,
93 pub(crate) params: HashMap<String, Value>,
94}
95impl fmt::Display for ErrorKind {
96 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97 write!(f, "{:#?}", self)
98 }
99}
100impl From<&str> for ErrorCode {
101 fn from(code: &str) -> Self {
102 match code.to_lowercase().as_str() {
103 | "ark" => Self::Ark,
104 | "iso 8601 date" | "date" => Self::Date,
105 | "doi" => Self::Doi,
106 | "email" => Self::Email,
107 | "epoch" | "unix epoch" => Self::Epoch,
108 | "image" => Self::Image,
109 | "ip6" => Self::Ip6,
110 | "isbn" => Self::Isbn,
111 | "kebabcase" => Self::KebabCase,
112 | "latitude" => Self::Latitude,
113 | "longitude" => Self::Longitude,
114 | "length" => Self::Length,
115 | "orcid" => Self::Orcid,
116 | "patent" => Self::Patent,
117 | "phone" => Self::Phone,
118 | "polygon" => Self::Polygon,
119 | "raid" | "raìd" => Self::Raid,
120 | "range" => Self::Range,
121 | "ror" => Self::Ror,
122 | "section" | "sections.approach" | "sections.areas" | "sections.capabilities" | "sections.impact" => Self::Section,
123 | "url" | "urls" => Self::Url,
124 | "year" => Self::Year,
125 | _ => Self::Other,
126 }
127 }
128}
129impl ErrorKind {
130 pub fn process(self, prefix: &str) -> Vec<ValidatorIssue> {
132 match self {
133 | ErrorKind::Validator(kind) => process(prefix, &kind),
134 | _ => vec![],
135 }
136 }
137}
138impl DocumentTarget for ValidatorIssue {
139 fn locator(&self) -> String {
141 let value = match self.path.as_deref() {
142 | Some(value) => match self.params.get("index") {
143 | Some(Value::Number(index)) => format!("{}[{}]", value, index),
144 | None | Some(_) => value.to_string(),
145 },
146 | None => self.code.to_string().to_uppercase(),
147 };
148 value.replace("r#", "")
149 }
150 fn source_text(&self) -> String {
152 match self.params.get("value") {
153 | Some(Value::Array(items)) => items.iter().map(|item| item.to_string()).collect::<Vec<String>>().to_markdown(),
154 | Some(value) => value.to_string(),
155 | None => " ".to_string(),
156 }
157 }
158 fn query(&self) -> DocumentQuery {
159 let locator = self.locator();
160 let query = DocumentQuery::new().with_path(DocumentPath::parse(&locator));
161 let value = match self.params.get("value") {
162 | Some(Value::Array(values)) => self
163 .params
164 .get("index")
165 .and_then(Value::as_u64)
166 .and_then(|index| values.get(index as usize)),
167 | value => value,
168 };
169
170 match value {
171 | Some(Value::String(value)) => query.with_value(value),
172 | Some(value) if !value.is_array() && !value.is_object() => query.with_value(value.to_string()),
173 | None | Some(_) => query,
174 }
175 }
176}
177pub fn process(path: &str, kind: &ValidationErrorsKind) -> Vec<ValidatorIssue> {
179 let is_wrapper_field = |value: &str| matches!(value, "license" | "month" | "year" | "postal_code");
180 match kind {
181 | ValidationErrorsKind::Field(errors) => {
182 let result = errors
183 .iter()
184 .map(|error| {
185 let ValidationError { code, message, params } = error.clone();
186 let path = if path.is_empty() { None } else { Some(path.to_string()) };
187 let message = message.map(|m| m.to_string()).unwrap_or_else(|| code.to_string());
188 let params = params
189 .into_iter()
190 .map(|(key, value)| (key.to_string(), value))
191 .collect::<HashMap<String, Value>>();
192 ValidatorIssue::init()
193 .code(ErrorCode::from(code.as_ref()))
194 .maybe_path(path)
195 .message(message)
196 .params(params)
197 .build()
198 })
199 .collect::<Vec<ValidatorIssue>>();
200 result
201 }
202 | ValidationErrorsKind::Struct(errors) => {
203 let result: Vec<ValidatorIssue> = errors
204 .clone()
205 .into_errors()
206 .into_iter()
207 .flat_map(|(field, kind)| {
208 let is_wrapper_recursion = matches!(kind, ValidationErrorsKind::Field(_)) && path == field && is_wrapper_field(field.as_ref());
209 let next_path = match (path.is_empty(), field.is_empty(), is_wrapper_recursion) {
210 | (true, false, _) => field.to_string(),
211 | (_, true, _) | (false, false, true) => path.to_string(),
212 | (false, false, false) => format!("{path}.{field}"),
213 };
214 process(&next_path, &kind)
215 })
216 .collect();
217 result
218 }
219 | ValidationErrorsKind::List(errors) => {
220 let result: Vec<ValidatorIssue> = errors
221 .iter()
222 .flat_map(|(index, error)| {
223 let prefix = if path.is_empty() { "".to_string() } else { path.to_string() };
224 let kind = ValidationErrorsKind::Struct(error.clone());
225 process(&format!("{prefix}[{index}]"), &kind)
226 })
227 .collect();
228 result
229 }
230 }
231}