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 =
113                paths::normalize_local_path(&paths::resolve_output_dir_path(base_path, relative));
114            write_text_file(&output_path, &content)?;
115            Ok(output_path.display().to_string())
116        }
117        _ => {
118            let uri = target.join_relative(relative);
119            let temp_dir = tempfile::tempdir()?;
120            let filename = Path::new(relative)
121                .file_name()
122                .and_then(|name| name.to_str())
123                .unwrap_or("report.json");
124            let temp_path = temp_dir.path().join(filename);
125            write_text_file(&temp_path, &content)?;
126            let client = cloud.client_for_context(resolver, target.storage(), context)?;
127            client.upload_from_path(&temp_path, &uri)?;
128            Ok(uri)
129        }
130    }
131}
132
133fn write_text_file(path: &Path, content: &str) -> FloeResult<()> {
134    let path = crate::io::storage::paths::normalize_local_path(path);
135    if let Some(parent) = path.parent() {
136        std::fs::create_dir_all(parent)?;
137    }
138    let tmp_path = temp_path(&path);
139    let mut file = std::fs::File::create(&tmp_path)?;
140    use std::io::Write;
141    file.write_all(content.as_bytes())?;
142    file.sync_all()?;
143    std::fs::rename(&tmp_path, &path)?;
144    Ok(())
145}
146
147fn temp_path(path: &Path) -> PathBuf {
148    let file_name = path
149        .file_name()
150        .and_then(|name| name.to_str())
151        .unwrap_or("report.json");
152    let tmp_name = format!("{file_name}.tmp-{}", unique_suffix());
153    path.parent().unwrap_or(path).join(tmp_name)
154}
155
156fn unique_suffix() -> String {
157    use std::time::{SystemTime, UNIX_EPOCH};
158    let nanos = SystemTime::now()
159        .duration_since(UNIX_EPOCH)
160        .map(|duration| duration.as_nanos())
161        .unwrap_or(0);
162    format!("{}-{}", std::process::id(), nanos)
163}