use std::path::Path;
use miette::Diagnostic;
use thiserror::Error;
use crate::bom::{self, BomError, BomFile, Part, Report};
use crate::cli::{BomArgs, BomFormat};
use crate::commands::check::{load_circuit_source, CheckError};
use crate::commands::target::{resolve_boards, TargetError};
use crate::config::FusibleConfig;
#[derive(Debug, Error, Diagnostic)]
pub enum BomCommandError {
#[error(transparent)]
#[diagnostic(transparent)]
Target(#[from] TargetError),
#[error(transparent)]
#[diagnostic(transparent)]
Source(#[from] CheckError),
#[error(transparent)]
#[diagnostic(transparent)]
Bom(#[from] BomError),
#[error("cannot write \"{path}\"")]
Write {
path: String,
#[source]
source: std::io::Error,
},
}
fn report(args: &BomArgs) -> Result<Report, BomCommandError> {
let boards = resolve_boards(args.target.clone())?;
let scoped = boards.len() > 1;
let mut parts: Vec<Part> = Vec::new();
for board in &boards {
let (_, source) = load_circuit_source(&board.entry)?;
let scope = board.name.as_deref().filter(|_| scoped);
parts.extend(bom::detect(&source, scope)?);
}
let (manifest, cfg) = FusibleConfig::nearest().unzip();
let names: Vec<&str> = boards.iter().filter_map(|b| b.name.as_deref()).collect();
let manifest = manifest.unwrap_or_else(|| "fusible.yml".into());
let root = manifest.parent().unwrap_or(Path::new("."));
let bodies = cfg.map_or_else(Vec::new, |c| bom::bodies_of(&c, &names, root));
let path = bom::bom_path(&manifest);
Ok(bom::build(parts, &bodies, &BomFile::load(&path)?))
}
pub fn run(args: BomArgs) -> Result<(), BomCommandError> {
let report = report(&args)?;
let text = match args.format {
BomFormat::Table => bom::table(&report),
BomFormat::Csv => bom::csv(&report),
BomFormat::Json => serde_json::to_string_pretty(&report).unwrap_or_default() + "\n",
};
match args.output {
Some(path) => std::fs::write(&path, text).map_err(|source| BomCommandError::Write {
path: path.display().to_string(),
source,
}),
None => {
print!("{text}");
Ok(())
}
}
}