use guicons_core::{IconEntrySource, IconManifest};
use miette::{LabeledSpan, MietteDiagnostic, NamedSource, Report, Severity};
use std::collections::HashMap;
use std::fs;
use std::ops::Range;
use std::path::{Path, PathBuf};
pub fn check(manifest_path: &Path) -> (usize, Vec<Report>) {
let (manifest, errors) = guicons_core::load_icon_manifest(manifest_path);
let mut source_cache: HashMap<PathBuf, String> = HashMap::new();
let mut reports: Vec<Report> = errors
.iter()
.map(|error| build_report(&error.file, error.span.clone(), error.message.clone(), Severity::Error, &mut source_cache))
.collect();
for entry in manifest.entries() {
match entry.source() {
IconEntrySource::File(path) if !path.exists() => {
reports.push(build_report(
entry.file(),
Some(entry.span()),
format!(
"icon manifest entry `{}` has a `file` source that doesn't exist: `{}`",
entry.key(),
display_path(path)
),
Severity::Error,
&mut source_cache,
));
}
IconEntrySource::Iconify(id) => {
if let Some(message) = unresolved_iconify_message(&manifest, id) {
reports.push(build_report(entry.file(), Some(entry.span()), message, Severity::Advice, &mut source_cache));
}
}
_ => {}
}
}
(manifest.entries().len(), reports)
}
fn unresolved_iconify_message(manifest: &IconManifest, id: &str) -> Option<String> {
if id.split_once(':').is_none() {
return Some(format!("iconify id `{id}` isn't in `provider:name` form - it will never resolve"));
}
let cache_path = guicons_net::iconify_cache_path(manifest.workspace_root(), id);
if cache_path.exists() {
return None;
}
Some(format!(
"iconify icon `{id}` isn't cached locally yet, so `check` can't confirm it resolves - run `icons fetch` (or set `GUICONS_ALLOW_NETWORK=1`) to fetch and verify it"
))
}
fn build_report(
file: &Path,
span: Option<Range<usize>>,
message: String,
severity: Severity,
source_cache: &mut HashMap<PathBuf, String>,
) -> Report {
let mut diagnostic = MietteDiagnostic::new(message).with_severity(severity);
if let Some(span) = &span {
diagnostic = diagnostic.with_label(LabeledSpan::at(span.start..span.end, "here"));
}
let mut report = Report::new(diagnostic);
if span.is_some() {
let source = source_cache
.entry(file.to_path_buf())
.or_insert_with(|| fs::read_to_string(file).unwrap_or_default())
.clone();
report = report.with_source_code(NamedSource::new(display_path(file), source));
}
report
}
fn display_path(path: &Path) -> String {
let relative = std::env::current_dir()
.ok()
.map(|cwd| fs::canonicalize(&cwd).unwrap_or(cwd))
.and_then(|cwd| path.strip_prefix(&cwd).ok().map(Path::to_path_buf));
let rendered = relative.as_deref().unwrap_or(path).display().to_string();
rendered.strip_prefix(r"\\?\").unwrap_or(&rendered).to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn check_reports_zero_errors_for_a_valid_manifest() {
let dir = tempdir().unwrap();
fs::write(dir.path().join("docker.svg"), "<svg/>").unwrap();
let path = dir.path().join("icons.gui.toml");
fs::write(&path, "[docker]\nfile = \"docker.svg\"\n").unwrap();
let (entry_count, reports) = check(&path);
assert_eq!(entry_count, 1);
assert!(reports.is_empty());
}
#[test]
fn check_reports_a_pretty_diagnostic_for_an_unknown_field() {
let dir = tempdir().unwrap();
let path = dir.path().join("icons.gui.toml");
fs::write(&path, "[docker]\nfile = \"docker.svg\"\nfile1 = \"docker.svg\"\n").unwrap();
let (_, reports) = check(&path);
assert_eq!(reports.len(), 1);
let rendered = format!("{:?}", reports[0]);
assert!(rendered.contains("unexpected field"), "{rendered}");
assert!(rendered.contains("file1"), "{rendered}");
}
#[test]
fn check_reports_an_error_for_a_file_source_pointing_at_a_nonexistent_asset() {
let dir = tempdir().unwrap();
let path = dir.path().join("icons.gui.toml");
fs::write(&path, "[docker]\nfile = \"does-not-exist.svg\"\n").unwrap();
let (entry_count, reports) = check(&path);
assert_eq!(entry_count, 1);
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].severity(), Some(Severity::Error));
let rendered = format!("{:?}", reports[0]);
assert!(rendered.contains("does-not-exist.svg"), "{rendered}");
}
#[test]
fn check_reports_advice_for_an_iconify_id_not_yet_cached() {
let dir = tempdir().unwrap();
let path = dir.path().join("icons.gui.toml");
fs::write(&path, "[docker]\niconify = \"mdi:home\"\n").unwrap();
let (entry_count, reports) = check(&path);
assert_eq!(entry_count, 1);
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].severity(), Some(Severity::Advice));
let rendered = format!("{:?}", reports[0]);
assert!(rendered.contains("mdi:home"), "{rendered}");
assert!(rendered.contains("isn't cached locally"), "{rendered}");
}
#[test]
fn check_reports_nothing_for_an_iconify_id_already_cached() {
let dir = tempdir().unwrap();
let path = dir.path().join("icons.gui.toml");
fs::write(&path, "[docker]\niconify = \"mdi:home\"\n").unwrap();
let cache_path = dir.path().join(".cache/guicons/mdi/home.svg");
fs::create_dir_all(cache_path.parent().unwrap()).unwrap();
fs::write(&cache_path, "<svg/>").unwrap();
let (_, reports) = check(&path);
assert!(reports.is_empty(), "{reports:?}");
}
}