use std::path::Path;
use hydra::report::{assemble, render_csv, render_html, render_txt, ReportContext, ReportTemplate};
use crate::{EXIT_INPUT, EXIT_INTERNAL, EXIT_IO, EXIT_OK};
#[derive(clap::Args, Debug)]
pub struct ReportArgs {
#[arg(long, value_name = "PATH")]
model: String,
#[arg(long, value_name = "PATH")]
results: String,
#[arg(long, value_name = "PATH")]
template: Option<String>,
#[arg(long, value_enum)]
format: Option<Format>,
#[arg(long, short = 'o', value_name = "PATH")]
out: Option<String>,
#[arg(long)]
no_timestamp: bool,
}
#[derive(clap::ValueEnum, Clone, Copy, Debug, PartialEq, Eq)]
enum Format {
Txt,
Csv,
Html,
Pdf,
}
pub fn run(cli: &ReportArgs, verbosity: &u8) -> i32 {
let _ = verbosity;
let model_bytes = match std::fs::read(&cli.model) {
Ok(bytes) => bytes,
Err(e) => {
crate::emit_error("io/fetch", &format!("cannot read model: {e}"), None, None);
return EXIT_INPUT;
}
};
let network = match hydra::io::parse(&model_bytes) {
Ok(network) => network,
Err(hydra::io::ParseError::NotSimulable(errors)) => {
for e in &errors {
crate::emit_error("validation/network", &e.to_string(), None, None);
}
return EXIT_INPUT;
}
Err(hydra::io::ParseError::Read(hydra::io::ReadError::ForeignDialect {
tool,
section,
})) => {
crate::emit_error(
"input/engine",
&format!(
"this is a {tool} model, not an EPANET one (it declares a [{section}] section)"
),
None,
None,
);
return EXIT_INPUT;
}
Err(hydra::io::ParseError::Read(hydra::io::ReadError::UnrecognisedFormat)) => {
crate::emit_error("input/format", "unrecognised file format", None, None);
return EXIT_INPUT;
}
Err(e) => {
crate::emit_error("input/parse", &e.to_string(), None, None);
return EXIT_INPUT;
}
};
let results_path = Path::new(&cli.results);
if let Err(e) = hydra::io::out_reader::read_metadata_checked(results_path) {
crate::emit_error("input/results", &e.to_string(), None, None);
return EXIT_INPUT;
}
let template = match &cli.template {
Some(path) => {
let json = match std::fs::read_to_string(path) {
Ok(json) => json,
Err(e) => {
crate::emit_error(
"io/fetch",
&format!("cannot read template {path}: {e}"),
None,
None,
);
return EXIT_INPUT;
}
};
match ReportTemplate::from_json(&json) {
Ok(template) => template,
Err(e) => {
crate::emit_error("input/parse", &e.to_string(), None, None);
return EXIT_INPUT;
}
}
}
None => ReportTemplate::covering("Simulation Report", hydra::report_catalog()),
};
let context = ReportContext {
generated_at: (!cli.no_timestamp)
.then(|| chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
source: vec![
("Model".into(), cli.model.clone()),
("Results".into(), cli.results.clone()),
],
};
let document = assemble(
&template,
hydra::report_catalog(),
context,
|id, options| hydra::produce_report_block(id, results_path, &network, options),
);
let format = cli.format.unwrap_or_else(|| {
match cli
.out
.as_deref()
.and_then(|p| Path::new(p).extension())
.and_then(|e| e.to_str())
.map(str::to_ascii_lowercase)
.as_deref()
{
Some("csv") => Format::Csv,
Some("html") | Some("htm") => Format::Html,
Some("pdf") => Format::Pdf,
_ => Format::Txt,
}
});
enum Rendered {
Text(String),
Binary(Vec<u8>),
}
let rendered = match format {
Format::Txt => Rendered::Text(render_txt(&document)),
Format::Csv => Rendered::Text(render_csv(&document)),
Format::Html => Rendered::Text(render_html(&document)),
Format::Pdf => match hydra::report::render_pdf(&document) {
Ok(bytes) => Rendered::Binary(bytes),
Err(e) => {
eprintln!("error: {e}");
return EXIT_INTERNAL;
}
},
};
match (&cli.out, rendered) {
(Some(path), Rendered::Text(text)) => {
if let Err(e) = std::fs::write(path, text) {
eprintln!("error: cannot write {path}: {e}");
return EXIT_IO;
}
}
(Some(path), Rendered::Binary(bytes)) => {
if let Err(e) = std::fs::write(path, bytes) {
eprintln!("error: cannot write {path}: {e}");
return EXIT_IO;
}
}
(None, Rendered::Text(text)) => print!("{text}"),
(None, Rendered::Binary(_)) => {
eprintln!("error: pdf output requires --out <PATH>");
return EXIT_INPUT;
}
}
EXIT_OK
}