Skip to main content

floe_core/report/
output.rs

1use std::path::{Path, PathBuf};
2
3use crate::io::storage::{paths, CloudClient, Target};
4use crate::report::{JsonReportFormatter, ReportFormatter};
5use crate::{config, report, FloeResult};
6
7pub struct ReportOutput<'a> {
8    target: &'a Target,
9    formatter: &'a dyn ReportFormatter,
10    cloud: &'a mut CloudClient,
11    resolver: &'a config::StorageResolver,
12}
13
14impl<'a> ReportOutput<'a> {
15    pub fn new(
16        target: &'a Target,
17        formatter: &'a dyn ReportFormatter,
18        cloud: &'a mut CloudClient,
19        resolver: &'a config::StorageResolver,
20    ) -> Self {
21        Self {
22            target,
23            formatter,
24            cloud,
25            resolver,
26        }
27    }
28
29    pub fn write_entity_report(
30        &mut self,
31        run_id: &str,
32        entity: &config::EntityConfig,
33        report: &report::RunReport,
34    ) -> FloeResult<String> {
35        let relative = report::ReportWriter::report_relative_path(run_id, &entity.name);
36        write_report(
37            self.target,
38            &relative,
39            self.formatter,
40            ReportPayload::Entity(report),
41            self.cloud,
42            self.resolver,
43            &format!("entity.name={}", entity.name),
44        )
45    }
46
47    pub fn write_summary_report(
48        &mut self,
49        run_id: &str,
50        report: &report::RunSummaryReport,
51    ) -> FloeResult<String> {
52        let relative = report::ReportWriter::summary_relative_path(run_id);
53        write_report(
54            self.target,
55            &relative,
56            self.formatter,
57            ReportPayload::Summary(report),
58            self.cloud,
59            self.resolver,
60            "report",
61        )
62    }
63}
64
65pub fn write_entity_report(
66    target: &Target,
67    run_id: &str,
68    entity: &config::EntityConfig,
69    report: &report::RunReport,
70    cloud: &mut CloudClient,
71    resolver: &config::StorageResolver,
72) -> FloeResult<String> {
73    let formatter = JsonReportFormatter;
74    let mut output = ReportOutput::new(target, &formatter, cloud, resolver);
75    output.write_entity_report(run_id, entity, report)
76}
77
78pub fn write_summary_report(
79    target: &Target,
80    run_id: &str,
81    report: &report::RunSummaryReport,
82    cloud: &mut CloudClient,
83    resolver: &config::StorageResolver,
84) -> FloeResult<String> {
85    let formatter = JsonReportFormatter;
86    let mut output = ReportOutput::new(target, &formatter, cloud, resolver);
87    output.write_summary_report(run_id, report)
88}
89
90enum ReportPayload<'a> {
91    Entity(&'a report::RunReport),
92    Summary(&'a report::RunSummaryReport),
93}
94
95fn write_report(
96    target: &Target,
97    relative: &str,
98    formatter: &dyn ReportFormatter,
99    payload: ReportPayload<'_>,
100    cloud: &mut CloudClient,
101    resolver: &config::StorageResolver,
102    context: &str,
103) -> FloeResult<String> {
104    // Storage-agnostic report write: local file or temp upload for cloud.
105    let content = match payload {
106        ReportPayload::Entity(report) => formatter.serialize_run(report)?,
107        ReportPayload::Summary(report) => formatter.serialize_summary(report)?,
108    };
109
110    match target {
111        Target::Local { base_path, .. } => {
112            let output_path = paths::resolve_output_dir_path(base_path, relative);
113            write_text_file(&output_path, &content)?;
114            Ok(output_path.display().to_string())
115        }
116        _ => {
117            let uri = target.join_relative(relative);
118            let temp_dir = tempfile::tempdir()?;
119            let filename = Path::new(relative)
120                .file_name()
121                .and_then(|name| name.to_str())
122                .unwrap_or("report.json");
123            let temp_path = temp_dir.path().join(filename);
124            write_text_file(&temp_path, &content)?;
125            let client = cloud.client_for_context(resolver, target.storage(), context)?;
126            client.upload_from_path(&temp_path, &uri)?;
127            Ok(uri)
128        }
129    }
130}
131
132fn write_text_file(path: &Path, content: &str) -> FloeResult<()> {
133    if let Some(parent) = path.parent() {
134        std::fs::create_dir_all(parent)?;
135    }
136    let tmp_path = temp_path(path);
137    let mut file = std::fs::File::create(&tmp_path)?;
138    use std::io::Write;
139    file.write_all(content.as_bytes())?;
140    file.sync_all()?;
141    std::fs::rename(&tmp_path, path)?;
142    Ok(())
143}
144
145fn temp_path(path: &Path) -> PathBuf {
146    let file_name = path
147        .file_name()
148        .and_then(|name| name.to_str())
149        .unwrap_or("report.json");
150    let tmp_name = format!("{file_name}.tmp-{}", unique_suffix());
151    path.parent().unwrap_or(path).join(tmp_name)
152}
153
154fn unique_suffix() -> String {
155    use std::time::{SystemTime, UNIX_EPOCH};
156    let nanos = SystemTime::now()
157        .duration_since(UNIX_EPOCH)
158        .map(|duration| duration.as_nanos())
159        .unwrap_or(0);
160    format!("{}-{}", std::process::id(), nanos)
161}