use super::readability::ReadabilityType;
use crate::analyzer::vale::ValeOutputItem;
use crate::prelude::HashMap;
use crate::util::ToMarkdown;
use bon::Builder;
use core::fmt;
use derive_more::Display;
use serde_json::Value;
use validator::{ValidationError, ValidationErrorsKind};
pub trait Location {
fn locator(&self) -> String;
fn source_text(&self) -> String;
}
#[derive(Clone, Debug, Display)]
#[display(rename_all = "lowercase")]
pub enum ErrorCode {
Ark,
Date,
Doi,
Email,
Epoch,
Image,
Ip6,
Isbn,
KebabCase,
Latitude,
Longitude,
Length,
Orcid,
Other,
Patent,
Phone,
Polygon,
Raid,
Range,
Ror,
Section,
Url,
Year,
}
#[derive(Clone, Debug)]
pub enum ErrorKind {
Readability((f64, ReadabilityType)),
Vale(Vec<ValeOutputItem>),
Validator(ValidationErrorsKind),
}
#[derive(Builder, Clone, Debug)]
#[builder(start_fn = init, on(String, into))]
pub struct ValidatorIssue {
pub(crate) code: ErrorCode,
pub(crate) path: Option<String>,
pub(crate) message: String,
pub(crate) params: HashMap<String, Value>,
}
impl fmt::Display for ErrorKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:#?}", self)
}
}
impl From<&str> for ErrorCode {
fn from(code: &str) -> Self {
match code.to_lowercase().as_str() {
| "ark" => Self::Ark,
| "iso 8601 date" | "date" => Self::Date,
| "doi" => Self::Doi,
| "email" => Self::Email,
| "epoch" | "unix epoch" => Self::Epoch,
| "image" => Self::Image,
| "ip6" => Self::Ip6,
| "isbn" => Self::Isbn,
| "kebabcase" => Self::KebabCase,
| "latitude" => Self::Latitude,
| "longitude" => Self::Longitude,
| "length" => Self::Length,
| "orcid" => Self::Orcid,
| "patent" => Self::Patent,
| "phone" => Self::Phone,
| "polygon" => Self::Polygon,
| "raid" | "raìd" => Self::Raid,
| "range" => Self::Range,
| "ror" => Self::Ror,
| "section" | "sections.approach" | "sections.areas" | "sections.capabilities" | "sections.impact" => Self::Section,
| "url" | "urls" => Self::Url,
| "year" => Self::Year,
| _ => Self::Other,
}
}
}
impl ErrorKind {
pub fn process(self, prefix: &str) -> Vec<ValidatorIssue> {
match self {
| ErrorKind::Validator(kind) => process(prefix, &kind),
| _ => vec![],
}
}
}
impl Location for ValidatorIssue {
fn locator(&self) -> String {
let value = match self.path.as_deref() {
| Some(value) => match self.params.get("index") {
| Some(Value::Number(index)) => format!("{}[{}]", value, index),
| None | Some(_) => value.to_string(),
},
| None => self.code.to_string().to_uppercase(),
};
value.replace("r#", "")
}
fn source_text(&self) -> String {
match self.params.get("value") {
| Some(Value::Array(items)) => items.iter().map(|item| item.to_string()).collect::<Vec<String>>().to_markdown(),
| Some(value) => value.to_string(),
| None => " ".to_string(),
}
}
}
pub fn process(path: &str, kind: &ValidationErrorsKind) -> Vec<ValidatorIssue> {
let is_wrapper_field = |value: &str| matches!(value, "license" | "month" | "year" | "postal_code");
match kind {
| ValidationErrorsKind::Field(errors) => {
let result = errors
.iter()
.map(|error| {
let ValidationError { code, message, params } = error.clone();
let path = if path.is_empty() { None } else { Some(path.to_string()) };
let message = message.map(|m| m.to_string()).unwrap_or_else(|| code.to_string());
let params = params
.into_iter()
.map(|(key, value)| (key.to_string(), value))
.collect::<HashMap<String, Value>>();
ValidatorIssue::init()
.code(ErrorCode::from(code.as_ref()))
.maybe_path(path)
.message(message)
.params(params)
.build()
})
.collect::<Vec<ValidatorIssue>>();
result
}
| ValidationErrorsKind::Struct(errors) => {
let result: Vec<ValidatorIssue> = errors
.clone()
.into_errors()
.into_iter()
.flat_map(|(field, kind)| {
let is_wrapper_recursion = matches!(kind, ValidationErrorsKind::Field(_)) && path == field && is_wrapper_field(field.as_ref());
let next_path = match (path.is_empty(), field.is_empty(), is_wrapper_recursion) {
| (_, true, _) => path.to_string(),
| (true, false, _) => field.to_string(),
| (false, false, true) => path.to_string(),
| (false, false, false) => format!("{path}.{field}"),
};
process(&next_path, &kind)
})
.collect();
result
}
| ValidationErrorsKind::List(errors) => {
let result: Vec<ValidatorIssue> = errors
.iter()
.flat_map(|(index, error)| {
let prefix = if path.is_empty() { "".to_string() } else { path.to_string() };
let kind = ValidationErrorsKind::Struct(error.clone());
process(&format!("{prefix}[{index}]"), &kind)
})
.collect();
result
}
}
}