use std::time::Duration;
use crate::{
context::Context,
error::FontspectorError,
prelude::FixFnResult,
status::CheckFnResult,
testable::{TestableCollection, TestableType},
CheckResult, Registry, Status, Testable,
};
pub type CheckId = String;
type CheckOneSignature = dyn Fn(&Testable, &Context) -> CheckFnResult;
type CheckAllSignature = dyn Fn(&TestableCollection, &Context) -> CheckFnResult;
#[derive(Clone)]
pub struct CheckFlags {
pub experimental: bool,
}
impl CheckFlags {
pub const fn default() -> Self {
Self {
experimental: false,
}
}
}
#[derive(Clone)]
pub enum CheckImplementation<'a> {
CheckOne(&'a CheckOneSignature),
CheckAll(&'a CheckAllSignature),
}
pub type HotfixFunction = dyn Fn(&mut Testable) -> FixFnResult;
#[derive(Clone)]
pub struct Check<'a> {
pub id: &'a str,
pub title: &'a str,
pub rationale: &'a str,
pub proposal: &'a [&'a str],
pub implementation: CheckImplementation<'a>,
pub hotfix: Option<&'a HotfixFunction>,
pub fix_source: Option<&'a dyn Fn(&Testable) -> FixFnResult>,
pub applies_to: &'a str,
pub flags: CheckFlags,
pub _metadata: Option<&'static str>,
}
unsafe impl Sync for Check<'_> {}
impl<'a> Check<'a> {
pub fn runs_on_collection(&self) -> bool {
matches!(self.implementation, CheckImplementation::CheckAll(_))
}
pub fn applies(&self, f: &'a TestableType, registry: &Registry) -> bool {
match (&self.implementation, f) {
(CheckImplementation::CheckAll(_), TestableType::Collection(_)) => true,
(CheckImplementation::CheckOne(_), TestableType::Single(f)) => registry
.filetypes
.get(self.applies_to)
.is_some_and(|ft| ft.applies(f)),
_ => false,
}
}
pub fn metadata(&self) -> serde_json::Value {
#[allow(clippy::expect_used)]
self._metadata
.map(|s| serde_json::from_str(s).unwrap_or_else(|_| panic!("Bad JSON in {}", self.id)))
.unwrap_or_default()
}
fn clarify_result(
&'a self,
fn_result: CheckFnResult,
filename: Option<&str>,
source_filename: Option<&str>,
section: Option<&str>,
context: &Context,
duration: Duration,
) -> CheckResult {
let subresults = match fn_result {
Ok(results) => results.collect::<Vec<_>>(),
Err(FontspectorError::Skip { code, message }) => vec![Status::skip(code, message)],
Err(e) => vec![Status::error(None, &format!("Error: {e}"))],
};
let mut res = if subresults.is_empty() {
vec![Status::pass()]
} else {
subresults
};
for status in res.iter_mut() {
status.process_override(&context.overrides);
}
CheckResult::new(self, filename, source_filename, section, res, duration)
}
pub fn run(
&'a self,
testable: &'a TestableType,
context: &Context,
section: Option<&str>,
) -> Option<CheckResult> {
log::info!("Running check {} on {:?}", self.id, testable);
match (&self.implementation, testable) {
(CheckImplementation::CheckAll(_), TestableType::Single(_)) => None,
(CheckImplementation::CheckOne(_), TestableType::Collection(_)) => None,
(CheckImplementation::CheckOne(check_one), TestableType::Single(f)) => {
#[cfg(not(target_family = "wasm"))]
let start = std::time::Instant::now();
let result = check_one(f, context);
#[cfg(not(target_family = "wasm"))]
let duration = start.elapsed();
#[cfg(target_family = "wasm")]
let duration = Duration::from_secs(0);
Some(self.clarify_result(
result,
f.filename.to_str(),
f.source.as_ref().and_then(|x| x.to_str()),
section,
context,
duration,
))
}
(CheckImplementation::CheckAll(check_all), TestableType::Collection(f)) => {
#[cfg(not(target_family = "wasm"))]
let start = std::time::Instant::now();
let result = check_all(f, context);
#[cfg(not(target_family = "wasm"))]
let duration = start.elapsed();
#[cfg(target_family = "wasm")]
let duration = Duration::from_secs(0);
Some(self.clarify_result(
result,
Some(&f.directory),
None,
section,
context,
duration,
))
}
}
}
}
pub fn return_result(problems: Vec<Status>) -> CheckFnResult {
if problems.is_empty() {
Ok(Status::just_one_pass())
} else {
Ok(Box::new(problems.into_iter()))
}
}