#![warn(missing_docs)]
use std::path::{Path, PathBuf};
pub mod config;
pub mod error;
pub mod error_catalog;
pub mod generate;
pub mod index;
pub mod lint;
pub mod platform;
mod rustdoc;
pub use config::{Config, Platform, Preset, UnknownPlatform};
pub use error::{Error, Result};
pub use error_catalog::{ErrorEntry, Snippet};
pub use generate::{Artifact, ArtifactLocation};
pub use index::{IndexedCrate, IndexedWorkspace};
pub use lint::{Diagnostic, Level};
#[derive(Debug, Clone)]
pub struct Report {
pub artifacts: Vec<Artifact>,
pub diagnostics: Vec<Diagnostic>,
}
impl Report {
pub fn has_errors(&self) -> bool {
self.diagnostics
.iter()
.any(|d| matches!(d.level, Level::Error))
}
}
pub fn run(workspace_root: &Path, config: &Config) -> Result<Report> {
let workspace = IndexedWorkspace::build(workspace_root, config)?;
let index_summary = format!("indexed {} crate(s)", workspace.crates.len());
let mut artifacts =
generate::render_all(&workspace, None).map_err(|source| Error::Generate {
source: Box::new(source),
index_summary: index_summary.clone(),
})?;
if config.emit_error_catalog {
let entries = error_catalog::extract(&workspace);
generate::render_error_catalog(&entries, &mut artifacts).map_err(|source| {
Error::Generate {
source: Box::new(source),
index_summary,
}
})?;
}
platform::apply_overlays(&workspace, &mut artifacts, &config.platforms)?;
let diagnostics = lint::lint(&workspace, &artifacts, config);
Ok(Report {
artifacts,
diagnostics,
})
}
fn resolve_path(artifact: &Artifact, out_dir: &Path, workspace_root: &Path) -> PathBuf {
let base = match artifact.location {
ArtifactLocation::OutDir => out_dir,
ArtifactLocation::WorkspaceRoot => workspace_root,
};
base.join(&artifact.path)
}
pub fn write_report(report: &Report, out_dir: &Path, workspace_root: &Path) -> Result<()> {
for artifact in &report.artifacts {
let path = resolve_path(artifact, out_dir, workspace_root);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)?;
}
std::fs::write(&path, artifact.body.as_bytes())?;
}
Ok(())
}
#[derive(Debug, Clone, Default)]
pub struct DiffSummary {
pub missing: Vec<String>,
pub modified: Vec<String>,
}
impl DiffSummary {
pub fn is_empty(&self) -> bool {
self.missing.is_empty() && self.modified.is_empty()
}
pub fn total(&self) -> usize {
self.missing.len() + self.modified.len()
}
pub fn paths(&self) -> impl Iterator<Item = &String> {
self.missing.iter().chain(self.modified.iter())
}
pub fn summary_message(&self, total_artifacts: usize) -> String {
if self.is_empty() {
format!("aidoc: check clean ({total_artifacts} artifact(s))")
} else if self.modified.is_empty() {
format!(
"aidoc: {} artifact(s) not on disk yet — run aidoc_gen to write them",
self.missing.len()
)
} else if self.missing.is_empty() {
format!(
"aidoc: {} artifact(s) content changed — run aidoc_gen to update",
self.modified.len()
)
} else {
format!(
"aidoc: {} missing + {} modified — run aidoc_gen",
self.missing.len(),
self.modified.len()
)
}
}
}
pub fn classify_diffs(
report: &Report,
out_dir: &Path,
workspace_root: &Path,
) -> Result<DiffSummary> {
let mut summary = DiffSummary::default();
for artifact in &report.artifacts {
let path = resolve_path(artifact, out_dir, workspace_root);
match std::fs::read(&path) {
Ok(bytes) => {
if bytes != artifact.body.as_bytes() {
summary.modified.push(artifact.path.clone());
}
}
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
summary.missing.push(artifact.path.clone());
}
Err(err) => return Err(err.into()),
}
}
Ok(summary)
}
pub fn diff_report(report: &Report, out_dir: &Path, workspace_root: &Path) -> Result<Vec<String>> {
let summary = classify_diffs(report, out_dir, workspace_root)?;
Ok(summary.paths().cloned().collect())
}